- strip-escaping-links.py: the /home[N]/<user>/ exemption ran on the RAW
target (`/home/u/../../etc/shadow` was exempt) and on usernames taken
from the archive's own top-dir names (an archive could name itself
cpmove-root). Now only the username WHP passes (extract.sh <username> =
IMPORT_USERNAME, validated) is trusted; the target must be normalized; and
it is RE-ROOTED at the link's <top>/homedir in the extract and kept only
if that resolves (realpath strict) inside it. No trusted name: nothing
exempt.
- scan-symlinks.php: same trusted-name rule; a non-normalized absolute
target is DANGEROUS outright (never prefix-compared).
- tests: /home/u/../../etc/shadow, /home/u/./x, re-rooted dangling,
re-rooted through an escaping link, another user's /home, an
archive-named user (cpmove-root) via extract.sh, no/invalid trusted name.
8 checks fail on 72b8fa9.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lexical relative-target check missed chains: `a -> b/../..` with
`b -> ../..`, or `l1 -> l2/../../..`, each resolve inside the archive
lexically but above the extract through the other link.
- scripts/lib/strip-escaping-links.py (new, the authority): after tar,
lstat-walk the WHOLE extract (never following links), realpath(strict)
every symlink, and remove + record (stripped_escaping_symlink) any that
resolves outside the extract or to it, dangles or loops. Repeats to a
fixed point; a walk error fails the extract. Own-account absolute
/home[N]/<user>/ targets are kept (they cannot resolve in the sandbox and
resolve only inside the customer's site container on WHP).
- scan-symlinks.php: two passes; a relative link whose resolution passes
through another symlink entry of the archive is DANGEROUS.
- fixtures/tests: cpmove-chain (both shapes) and a hand-built tree for the
post-extract pass (chains, /etc, dangling .bak sidecar, escaping .bak,
loop, link to the top). 3 chain checks fail on a226b49.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>