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>
114 lines
4.0 KiB
Bash
Executable File
114 lines
4.0 KiB
Bash
Executable File
#!/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"
|