Containment check for relationship targets resolved from untrusted OOXML
parts (CWE-22). Two layers, and the canonical one decides (#594):
Lexical: the resolved path, as spelled, must stay under unpackedDir
(path.relative must not escape via .. or go absolute). This judges
the spellings as passed, so on a case-insensitive volume it reads an
in-root target spelled unlike the root as an escape — it is a filter,
never the verdict, while layer 2 can run.
Symlinks and case: a purely lexical check is defeated by a symlink
inside the unpacked tree pointing outward (link/secret where link
→ /etc), and blind to spelling: Node's JS realpathSync keeps the
case a path is written in, so only realpathSync.native reports the
on-disk case (#587). The root is canonicalized and the deepest
EXISTING ancestor of the target is resolved with realpathSync.native
and re-checked against it: a target whose canonical path lies under
the canonical root is contained under any spelling; non-existent
trailing components cannot change the verdict (callers report missing
targets as broken anyway).
The lexical layer decides only when the realpath layer cannot run — when
the root itself cannot be canonicalized, or when nothing along the target
resolves — and it fails closed for a lexical escape in both cases.
Returns the target as passed when contained, null on escape.
Not race-resistant across the check and a later open — acceptable for a
validator scanning an unpacked tree it controls; flagged for any future
fd-based (O_NOFOLLOW) hardening.
Containment check for relationship targets resolved from untrusted OOXML parts (CWE-22). Two layers, and the canonical one decides (#594):
unpackedDir(path.relativemust not escape via..or go absolute). This judges the spellings as passed, so on a case-insensitive volume it reads an in-root target spelled unlike the root as an escape — it is a filter, never the verdict, while layer 2 can run.link/secretwherelink→/etc), and blind to spelling: Node's JSrealpathSynckeeps the case a path is written in, so onlyrealpathSync.nativereports the on-disk case (#587). The root is canonicalized and the deepest EXISTING ancestor of the target is resolved withrealpathSync.nativeand re-checked against it: a target whose canonical path lies under the canonical root is contained under any spelling; non-existent trailing components cannot change the verdict (callers report missing targets as broken anyway).The lexical layer decides only when the realpath layer cannot run — when the root itself cannot be canonicalized, or when nothing along the target resolves — and it fails closed for a lexical escape in both cases.
Returns the target as passed when contained,
nullon escape. Not race-resistant across the check and a later open — acceptable for a validator scanning an unpacked tree it controls; flagged for any future fd-based (O_NOFOLLOW) hardening.