Skip to content

Resolve symlinks before the previous-source-map containment check - #2125

Merged
ai merged 1 commit into
postcss:mainfrom
drengir1:drengir1-patch-1
Aug 4, 2026
Merged

Resolve symlinks before the previous-source-map containment check#2125
ai merged 1 commit into
postcss:mainfrom
drengir1:drengir1-patch-1

Conversation

@drengir1

@drengir1 drengir1 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Follow-up to the hardening note in GHSA-r395-w6c4-rj3r — thanks for the invitation to send this as a normal PR.

What

loadFile() guards the previous-source-map load with:

let rel = relative(dirname(cssFile), path)
if (rel === '..' || rel.startsWith('..' + sep) || isAbsolute(rel)) return undefined

path.relative() is textual and never resolves symlinks. A symlink at or below the CSS file's own directory therefore returns something like "link.map" — no .., not absolute — the guard passes, and readFileSync() follows the link out of the tree.

This resolves both sides with realpathSync before comparing, so the check rejects by outcome rather than by syntax. A missing or dangling target falls back to the literal path; that cannot escape, because the existing existsSync() gate still applies.

Verified against 8.5.23

before after
symlink → outside the tree read blocked
directory symlink → outside read blocked
14 textual traversal payloads (encoded %2f/%5c, double-encoded, backslash, ....//, uppercase .MAP, Windows drive, file://, UNC, null byte, fullwidth solidus, newline) blocked blocked
control — legitimate in-tree .map still loads loads loads
CVE-2026-69153 (from unset) fixed fixed

The control row is the one that matters: the patch must not simply stop loading maps, and it doesn't.

Scope

Not a security fix. Reaching this needs an attacker who can already place a symlink inside the project tree, and the threat model here is untrusted CSS content, not an untrusted filesystem — which is why I sent it as a hardening note rather than an advisory in the first place.

Happy to add a regression test in test/previous-map.test.ts alongside the existing traversal cases (create project/link.map -> <outside>/secret.map, assert the map is not loaded; skipped on Windows, where unprivileged symlink creation fails). Say the word and I'll push it to this branch.

relative() is textual and never resolves symlinks, so a symlink at or below the CSS file’s directory passes the guard and readFileSync() follows it out of the tree. Resolve both sides with realpathSync first so the check rejects by outcome rather than syntax.
@ai
ai merged commit 29e9e00 into postcss:main Aug 4, 2026
10 checks passed
@ai

ai commented Aug 4, 2026

Copy link
Copy Markdown
Member

Thanks. I will release it later with some next feature or fix.

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.

2 participants