scan-symlinks: relative targets escaping the archive or the homedir are DANGEROUS (N2-1)

A relative symlink was skipped as safe (`if ($target[0] !== '/') continue`),
so `cpmove-u/sds -> ../../../../root/.my.cnf` survived extraction and WHP
parsed the host file as the subdomain list. Resolve relative targets
lexically from the link's own directory: reaching or climbing above the
archive root, or a link under <top>/homedir/ resolving outside that homedir,
is DANGEROUS (stripped by extract.sh). Same rule as WHP's
CpanelBackupImporter::relativeSymlinkEscape().

The clean fixture's about-shortcut (../public_html/about.txt resolved
outside homedir/) now points at public_html/about.txt as its comment meant.
New cpmove-relesc fixture + checks (5 fail on 4a43d26).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shadowdaoandClaude Opus 5.5 committed 2026-10-20 10:42:15 -07:00
1 parent 4a43d26de7
commit a226b496a2
3 files changed
+79 -3

No files matched your search

+43 -1
View File
@@ -90,6 +90,32 @@ $dangerousPrefixes = [
'/dev',
];
/**
* Lexical resolution of a relative link from its own directory in the
* archive: a reason when it reaches/climbs above the archive root, or when a
* link under <top>/homedir/ resolves outside that homedir; NULL otherwise.
*/
function relative_symlink_escape(string $archivePath, string $target): ?string {
$ap = preg_replace('#^(\./)+#', '', $archivePath);
$dir = array_values(array_filter(explode('/', (string) $ap), fn($x) => $x !== '' && $x !== '.'));
array_pop($dir);
$home = (count($dir) >= 2 && $dir[1] === 'homedir') ? [$dir[0], 'homedir'] : null;
$stack = $dir;
foreach (explode('/', $target) as $seg) {
if ($seg === '' || $seg === '.') continue;
if ($seg === '..') {
if (count($stack) <= 1) return 'escapes the archive root';
array_pop($stack);
continue;
}
$stack[] = $seg;
}
if ($home !== null && (count($stack) < 2 || $stack[0] !== $home[0] || $stack[1] !== 'homedir')) {
return 'escapes the account homedir';
}
return null;
}
$findings = [];
$cpanelUsername = null;
@@ -111,7 +137,23 @@ while (($line = fgets($fh)) !== false) {
$archivePath = $parts[5];
$target = $right;
if ($target === '' || $target[0] !== '/') continue;
// N2-1: a RELATIVE target that climbs out of the archive (or out of
// the account's homedir/) is DANGEROUS (stripped), not safe: GNU tar
// extracts `cpmove-u/sds -> ../../../../root/.my.cnf` as-is. Same rule
// as WHP's CpanelBackupImporter::relativeSymlinkEscape().
if ($target !== '' && $target[0] !== '/') {
$escape = relative_symlink_escape($archivePath, $target);
if ($escape !== null) {
$findings[] = [
'type' => 'DANGEROUS',
'archive_path' => $archivePath,
'target' => $target,
'reason' => "relative target $escape",
];
}
continue;
}
if ($target === '') continue;
if ($cpanelUsername === null) {
if (preg_match('#^cpmove-([^/]+)/#', $archivePath, $m)) {