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>
This commit is contained in:
Claude (bootstrap)
2026-05-30 19:56:57 -07:00
commit 5487dfc8f1
17 changed files with 2008 additions and 0 deletions

113
tests/build-fixtures.sh Executable file
View File

@@ -0,0 +1,113 @@
#!/usr/bin/env bash
#
# build-fixtures.sh — generate synthetic cpmove tarballs for testing.
#
# Two fixtures are built:
#
# cpmove-clean.tar.gz — a minimal cpmove with a benign homedir, one
# wp-style SQL dump with ENGINE=MyISAM tables
# and a clean siteurl, and a user-internal
# relative symlink (must not trigger).
#
# cpmove-alfa.tar.gz — same shape PLUS an ALFA-style symlink:
# `cpmove-testuser/homedir/.../alfasymlink -> /etc`
# — the pre-extract scan MUST refuse this.
#
# Run: bash tests/build-fixtures.sh
# Output: tests/fixtures/cpmove-clean.tar.gz, tests/fixtures/cpmove-alfa.tar.gz
set -euo pipefail
FIXTURES_DIR="$(cd "$(dirname "$0")" && pwd)/fixtures"
mkdir -p "$FIXTURES_DIR"
USER=testuser
DOMAIN=example.com
build_common_tree() {
local root="$1"
mkdir -p "$root/cpmove-$USER"/{homedir/public_html,mysql,userdata,addons,sds,ssl}
# main userdata
cat > "$root/cpmove-$USER/userdata/main" <<EOF
main_domain: $DOMAIN
user: $USER
EOF
# per-domain userdata file
cat > "$root/cpmove-$USER/userdata/$DOMAIN" <<EOF
servername: $DOMAIN
documentroot: /home/$USER/public_html
user: $USER
EOF
# benign content
echo "<?php phpinfo();" > "$root/cpmove-$USER/homedir/public_html/index.php"
echo "Hello world." > "$root/cpmove-$USER/homedir/public_html/about.txt"
# benign user-internal relative symlink — must NOT trigger the scan
ln -sf "../public_html/about.txt" "$root/cpmove-$USER/homedir/about-shortcut"
# one synthetic WordPress mysql dump with ENGINE=MyISAM + a clean siteurl
cat > "$root/cpmove-$USER/mysql/${USER}_wp.sql" <<EOF
-- Synthetic WP dump for cpanel-importer fixtures.
CREATE TABLE \`wp_options\` (
option_id bigint(20) NOT NULL,
option_name varchar(191) NOT NULL,
option_value longtext NOT NULL,
autoload varchar(20) NOT NULL DEFAULT 'yes'
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
CREATE TABLE \`wp_posts\` (
ID bigint(20) NOT NULL,
post_content longtext NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
CREATE TABLE \`wp_users\` (
ID bigint(20) NOT NULL,
user_login varchar(60) NOT NULL,
user_pass varchar(255) NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
INSERT INTO \`wp_options\` (option_id, option_name, option_value, autoload) VALUES
(1, 'siteurl', 'https://$DOMAIN', 'yes'),
(2, 'home', 'https://$DOMAIN', 'yes'),
(3, 'blogname', 'Hello', 'yes'),
(4, 'template', 'twentytwentyfour', 'yes'),
(5, 'stylesheet', 'twentytwentyfour', 'yes');
INSERT INTO \`wp_users\` VALUES (1, 'admin', 'doesnotmatter');
EOF
}
# ---- cpmove-clean.tar.gz --------------------------------------------------
CLEAN_TMP="$(mktemp -d)"
trap 'rm -rf "$CLEAN_TMP" "$ALFA_TMP" 2>/dev/null || true' EXIT
build_common_tree "$CLEAN_TMP"
tar -C "$CLEAN_TMP" -czf "$FIXTURES_DIR/cpmove-clean.tar.gz" "cpmove-$USER"
echo "wrote $FIXTURES_DIR/cpmove-clean.tar.gz ($(stat -c%s "$FIXTURES_DIR/cpmove-clean.tar.gz") bytes)"
# ---- cpmove-alfa.tar.gz ---------------------------------------------------
#
# Build the SAME tree, then add an ALFA-shell-style symlink pointing at
# /etc. This is the exact vector that wiped whp02 — the importer's
# recursive walker followed the symlink and unlink()'d every file in
# /etc. Our pre-extract scan MUST refuse to extract this tarball.
ALFA_TMP="$(mktemp -d)"
build_common_tree "$ALFA_TMP"
mkdir -p "$ALFA_TMP/cpmove-$USER/homedir/public_html/$DOMAIN/ALFA_DATA"
echo "<?php /* ALFA shell stub */ ?>" \
> "$ALFA_TMP/cpmove-$USER/homedir/public_html/$DOMAIN/ALFA_DATA/index.php"
# THE attack: absolute-target symlink to /etc.
ln -sf "/etc" "$ALFA_TMP/cpmove-$USER/homedir/public_html/$DOMAIN/ALFA_DATA/root"
tar -C "$ALFA_TMP" -czf "$FIXTURES_DIR/cpmove-alfa.tar.gz" "cpmove-$USER"
echo "wrote $FIXTURES_DIR/cpmove-alfa.tar.gz ($(stat -c%s "$FIXTURES_DIR/cpmove-alfa.tar.gz") bytes)"
echo ""
echo "fixtures built:"
ls -la "$FIXTURES_DIR"

0
tests/fixtures/.gitkeep vendored Normal file
View File