Skip to content

Contain @import file loads within base_dir (arbitrary local file read) - #192

Open
Guilhermemury wants to merge 1 commit into
premailer:masterfrom
Guilhermemury:fix/import-base-dir-lfi
Open

Guilhermemury wants to merge 1 commit into
premailer:masterfrom
Guilhermemury:fix/import-base-dir-lfi

Conversation

@Guilhermemury

@Guilhermemury Guilhermemury commented Oct 2, 2026 •

Copy link
Copy Markdown

Fixes the base_dir arm of the @import handler — the local-file-read sibling
of the SSRF/file:// issue fixed in GHSA-9pmc-p236-855h / CVE-2026-53727.
Tracked privately as GHSA-w3mx-4vv9-hjpg (fix #1 from that advisory).

Problem

When the caller sets :base_dir without :base_uri (the default for Premailer
local-file input), add_block!'s @import handler passed the import path
straight into load_file! → File.expand_path + File.read with no
containment. CSS containing @import "/etc/passwd"; (absolute) or
@import "../../secret"; (traversal) could read an arbitrary local file, merge
it into the ruleset, and — via Premailer — inline it into the output HTML. The
GHSA-9pmc-p236-855h fix only gated load_uri! / the base_uri arm; this arm
and load_file! were untouched.

Fix

  • The @import handler marks these loads (from_import: true) and threads an
    import_root (the top-level base_dir) through nested imports.
  • load_file! contains from_import loads within import_root: absolute paths
    and .. that escape the root are refused (RemoteFileError when
    io_exceptions is on, otherwise a silent no-op).
  • Legitimate .. imports that stay inside the root still work (e.g.
    subdir/import2.css → ../simple.css, as in the existing fixtures).
  • Direct load_file! callers — the explicit, trusted local-file API — are
    unaffected.

Tests

New test/test_css_parser_import_lfi.rb (7 tests): absolute + traversal
refusals, io_exceptions:false no-op, in-root imports still load, ..-within-
root still loads, direct load_file! unaffected. Full suite green
(246 runs, 758 assertions, 0 failures); RuboCop clean.

The base_dir arm of add_block!'s @import handler routed the
attacker-controlled import path straight into load_file! ->
File.expand_path + File.read with no containment. CSS containing
`@import "/etc/passwd"` or `@import "../../secret"` could therefore read
arbitrary local files: the content is merged into the ruleset and, via
Premailer's local-file input (base_dir set, base_uri nil), inlined into
the produced HTML.

This is the base_dir sibling of the file://-via-base_uri hole closed in
GHSA-9pmc-p236-855h / CVE-2026-53727, which only gated load_uri! and
left this arm (and load_file!) untouched.

Fix: mark @import-sourced load_file! calls (from_import) and contain the
resolved path within the import root -- the top-level base_dir, threaded
through nested imports. Absolute paths and `..` traversal that escape the
root are refused (RemoteFileError when io_exceptions is on, otherwise a
silent no-op). `..` that stays inside the root still works, and direct
load_file! callers (the trusted local-file API) are unaffected.

Adds test/test_css_parser_import_lfi.rb covering the refusals, the
in-root cases, and the direct-caller API.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 2, 2026 23:17

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Follow @import rules

2 participants