Review I-1: WHP's CpanelArchivePaths::selectTopDir() takes the first
non-hidden top dir of any name (cpmove-<u>, cPanel's user download
backup-<date>_<u>/, acct/ ...), but the link classifier, the post-extract
verify and scan-files' --skip-mail only matched cpmove-*. Measured by the
reviewer: an acct/ archive kept a ../../etc symlink in its Maildir, and
ClamAV would quarantine messages OUT of a backup-* Maildir (=> Verified
with mail missing).
- mail-tree-links.py: MAIL = ^([^/]+)/homedir/mail; verify walks every
top-level dir, and fails on a top-level or homedir symlink instead of
walking through it. Covering every top dir is a superset of whichever
one WHP picks, so multi-top-dir archives need no special case.
- scan-files.php: --skip-mail patterns use [^/]+ for the top dir.
- M-7: entrypoint refuses IMPORT_INCLUDE_MAIL=true as uid 0 (the verify's
os.access check is meaningless for root; WHP runs --user 999:999).
- M-2: the mail-tree refusals tell the operator the same backup may
import with "Import without email".
- M-4: run-tests.sh checks the env/marker contract against a WHP checkout
when WHP_REPO is set.
- Fixtures cpmove-acct / cpmove-backupdir (the mail fixture under acct/
and backup-10.20.2026_10-00-00_testuser/). 13 checks fail on d4a63fb.
Docker: backup-* Maildir EICAR not quarantined, public_html one is;
--user 0 with the opt-in => status failed.
The default (no-opt-in) mail exclude keeps its historical cpmove-* scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Security review of 52b1e65:
1. (HIGH) tarfile's stream could enumerate differently from GNU tar. Now
extract.sh captures tar's exit code (`|| TAR_RC=$?`), ALWAYS runs the
post-extract verify on whatever tar wrote, and any failure (tar,
verify, either pre-extract scan) removes $DEST before exiting non-zero.
The classifier also refuses (exit 3) on any stream exception and when
its member count differs from GNU tar's own escape-quoted listing.
Measured: a lone zero block mid-archive stops BOTH readers at the same
member (no -i is used), so this is a guard, not a known differential.
2. (MEDIUM) os.walk swallowed unreadable directories. verify now passes
onerror (recorded => fail) and refuses any directory that is not
readable+searchable. 52b1e65 exited 0 on the unreadable fixture.
3. (MEDIUM) a refused tree stayed in the bind-mounted output. fail_extract
opens up 0000 dirs (find -P -type d, never follows links), then
rm -rf -- "$DEST".
Fixtures: cpmove-bad-{unreadable,tarfail,truncated}; a fake tar that lists
one extra member proves the count check. 14 checks fail on 52b1e65.
Docker: unreadable / tarfail runs end with status failed, failed_stage
extract and no extract-work left in /host/sanitized/<id>.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Security review of 4788ca4 (inline python link classifier):
1. Names like cpmove-x/./homedir/mail/..., homedir//mail, homedir/../homedir
or /cpmove-x/... were matched differently by the classifier than by tar,
so a mail-tree symlink could escape the strip (measured: the dot, dslash
and abs fixtures extracted with exit 0 on 4788ca4).
2. A newline in a member name split one --exclude-from pattern in two.
Moved the policy to scripts/lib/mail-tree-links.py:
- scan (pre-extract, an optimisation): REFUSES the archive (exit 3) on any
control character in a member name or link target, or any non-canonical
member name / hard-link target (absolute, '..', or normpath() differing
after one leading './'). Symlink targets are not canonical-checked:
ordinary cPanel links (../public_html) are legitimate outside the mail
tree, and every symlink inside it is stripped anyway.
- verify (post-extract, the authority): lstat-walks each mail tree; exit 4
on any symlink, or any regular file whose st_nlink exceeds its names
inside that same mail tree (a link leaving it). In-tree Dovecot hard-link
copies (maildir_copy_with_hardlinks) are kept; a blanket nlink>1 refusal
would drop real mail.
- Fixtures: cpmove-bad-{dot,dslash,dotdot,abs,newline}; verify tests for
symlinks and hard links into/out of/between mail trees surviving the
pre-filter. 14 of the new checks fail on 4788ca4.
Only in IMPORT_INCLUDE_MAIL=true mode; the default path is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>