Commit Graph
5 Commits
Author SHA1 Message Date
shadowdaoandClaude Opus 5.5 4a43d26de7 extract: mail-tree policy covers ANY top-level dir, not just cpmove-* (I-1)
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>
2026-10-20 09:59:41 -07:00
shadowdaoandClaude Opus 5.5 8217397154 extract: opt-in mail tree (IMPORT_INCLUDE_MAIL=true) + mail_included marker
WHP's cPanel mailbox import needs cpmove-*/homedir/mail, which extract.sh
always excluded: in sanitization-container mode every mailbox was queued
with no Maildir and "verified" empty (B5, whp02 canary 2026-10-20).

- IMPORT_INCLUDE_MAIL=true (exactly "true"; default off) keeps the mail
  tree in the extract. Default behaviour is unchanged. The DANGEROUS
  symlink strips still apply inside the mail tree (scan-symlinks.php lists
  every archive entry).
- scan-files.php --skip-mail (passed by entrypoint.sh with the opt-in):
  clamscan --exclude-dir for the mail tree, the file count skips it, and a
  hit inside it is never quarantined. Logged with the reason: mail is
  copied verbatim into the customer's own mailbox and verified against the
  source, and multi-GB maildirs would dominate the run.
- report.json gains "mail_included" (true only with the opt-in) so the
  panel can tell this image from an older one, which never writes the key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 09:04:44 -07:00
Claude (bootstrap)andClaude Opus 4.7 4888f85b54 scan-files: silence open_basedir warnings during walk
cpanel-importer Build and Push / Build-and-Push (push) Successful in 53s
Container run on darkside completed end-to-end successfully (170,753
files scanned, 78 symlinks skipped via filter, 65 quarantined, walk
errors = 0) — but the import log was flooded with PHP Warnings on
every cpmove-internal symlink whose absolute target points outside
the container's open_basedir allow-list:

  PHP Warning:  is_link(): open_basedir restriction in effect.
  File(/host/sanitized/.../cybercoveconsulting.com/wp-content/db.php)
  is not within the allowed path(s): (/host:/tmp:...)

The actual code path was correct — is_link() still returns true when
warning, so the filter callback properly skipped these. But the noise
made the streamed [container] log on the panel side unreadable
(hundreds of warning lines per real signal line).

Root cause: PHP's open_basedir check normalizes via realpath() even
for is_link/is_file. cpmove symlinks like:
  db.php -> /home/<user>/<addon>.com/wp-content/db.php
  access-logs -> /usr/local/apache/domlogs/<user>
  cpanel-styled -> /usr/local/cpanel/base/frontend/.../glass
have absolute targets that don't exist anywhere in the container
(no /home, no /usr/local), so realpath() can't normalize them under
any allow-list entry. PHP fires Warning, returns the lstat answer
anyway, and our filter handles the skip correctly.

Fix: a scoped set_error_handler around the walk that suppresses ONLY
E_WARNINGs containing 'open_basedir restriction'. Non-open_basedir
warnings still surface. The handler is restored immediately after the
file-count loop, so subsequent stages (clamscan output parsing,
quarantine actions) keep the default error reporting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 12:08:40 -07:00
Claude (bootstrap)andClaude Opus 4.7 cda432e808 scan-files: skip symlinks during file walk to avoid open_basedir aborts
cpanel-importer Build and Push / Build-and-Push (push) Successful in 56s
cPanel cpmove tarballs contain symlinks with absolute targets pointing at
the SOURCE server's filesystem (e.g. addon docroots symlinked to
/home/<user>/<addondomain>.com/ on the cPanel host). After extract into
the container, those symlinks dangle — their targets don't exist in the
container's namespace AND are not under any open_basedir-allowed prefix.

PHP's SplFileInfo::isFile() (called from the RecursiveIteratorIterator
file-count loop) follows symlinks. The realpath check against
open_basedir then fires on the symlink TARGET, not the link path, and
throws RuntimeException mid-iteration — aborting the entire scan
without writing report.json. Surfaced on darkside import as:

  PHP Fatal error: Uncaught RuntimeException: SplFileInfo::isFile():
  open_basedir restriction in effect. File(/host/sanitized/.../
  cybercoveconsulting.com/wp-content/db.php) is not within the allowed
  path(s): (/host:/tmp:/opt/whp:/scripts:/var/lib/clamav:...)

Fix is two-layered:

1. RecursiveCallbackFilterIterator pre-filters symlinks via is_link()
   before they reach hasChildren/isFile. is_link is open_basedir-safe
   (it stats the link itself, doesn't resolve). Skipped count is
   reported on STDERR so operators see what was skipped.

2. try/catch around the per-entry isFile() as a defense-in-depth
   layer — if any other fs op throws mid-walk (race, planted device
   node, etc.) we count it as a walk_error and continue, not abort.

Note that clamscan already walks the extract tree on its own pass and
its default symlink posture is "don't follow" — the same posture we
want here. Symlink-as-file would also be useless to quarantine (it's
a 0-byte fs entry whose target is the actual artifact). Skipping
symlinks therefore doesn't miss anything.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 11:40:44 -07:00
Claude (bootstrap)andClaude Opus 4.7 5487dfc8f1 Initial bootstrap: cpanel-importer sanitization sandbox
Skeleton for the cpanel-importer Docker container — a one-shot
sandbox the WHP panel invokes BEFORE extracting a customer cpmove
tarball. See cpanel-import-container-spec.md (in /workspace/) for the
full design.

What this ships in v1.0:

- Dockerfile: almalinux:10-minimal + PHP 8.4 (Remi) + ClamAV 1.4 +
  SaneSecurity Foxhole.PHP rules + tar/mariadb-client/rsync. Runs as
  UID 999 (whp-import) via the panel-side --user 999:999 flag.

- scripts/entrypoint.sh: validates env, runs (optional) freshclam,
  drives extract -> scan-files -> scan-dbs -> rsync -> report.json.

- scripts/extract.sh + scripts/lib/scan-symlinks.php: pre-extract
  symlink scan ported standalone from
  web-files/libs/CpanelBackupImporter.php (the existing 2026-05-29
  whp02 destruction-vector fix). Aborts with exit 3 before tar runs
  if any DANGEROUS symlink is found.

- scripts/scan-files.php: ClamAV walk + classify-and-action. v1.0
  ships with an empty cleaner registry — every hit is
  QUARANTINE_ONLY. Cleaner hooks are stubbed for v1.1.

- scripts/scan-dbs.php: regex MyISAM -> InnoDB rewrite (always
  applied), WordPress identification, and ONE WP content scan check
  (siteurl_external_domain). v1.1 will grow the check set.

- scripts/lib/safety-net.php: container-narrow open_basedir
  allow-list, much tighter than the panel-side one.

- .gitea/workflows/build-push.yaml: builds + smoke-tests +
  PHP-syntax-checks + bash-syntax-checks before pushing to
  repo.anhonesthost.net/cloud-hosting-platform/cpanel-importer.

- tests/build-fixtures.sh: builds cpmove-clean.tar.gz (benign WP
  dump) and cpmove-alfa.tar.gz (the ALFA-shell symlink-to-/etc
  vector) for local end-to-end testing.

- README.md / CONTRIBUTING.md: docker-run invocation, bind-mount
  catalog, report.json schema, how to add a cleaner pattern or a WP
  scan signature.

Local acceptance test results:
- clean fixture -> status=completed, 3 MyISAM->InnoDB, no flags, 0
- ALFA fixture -> exit 1, status=failed, failed_stage=extract,
  "tarball contains dangerous symlinks; aborting" on stderr
- compromised-siteurl fixture -> imported_into_new_server=false,
  .flagged file written, summary_for_panel.show_alert=true

Image size: 197 MB compressed (gzipped docker save), ~397 MB unique
layers extracted. Well under the spec's 600 MB compressed / 1.2 GB
extracted budget.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 19:56:57 -07:00