T09 · Insecure Skill Coding Practices
- Location
scripts/quarantine_path.py:27- Finding
Archive Path Traversal Through Unvalidated Label
- Content
View full analysis
Vulnerability Details
File Location:
scripts/quarantine_path.py, lines 27-43
Vulnerability Type: Path traversal and archive-boundary escape
Risk Level: MediumVulnerable Code
python ap.add_argument('--label', required=True, help='Target folder/file name inside the archive root') ap.add_argument('--note') args = ap.parse_args() source = Path(args.source) archive_root = Path(args.archive_root) target = archive_root / args.label manifest = archive_root / 'MANIFEST.md' if not source.exists() and not source.is_symlink(): raise SystemExit(f'Source does not exist: {source}') if target.exists(): raise SystemExit(f'Target already exists: {target}') archive_root.mkdir(parents=True, exist_ok=True) shutil.move(str(source), str(target)) append_manifest(manifest, source, target, args.note) print(f'Moved {source} -> {target}') print(f'Updated manifest: {manifest}')Technical Analysis
The script states that
--labelis a target name inside the archive root, but it accepts the argument without validating its path semantics. The expressionarchive_root / args.labeldoes not enforce containment:- An absolute label causes
pathlibto discardarchive_root. - A relative label containing
..can resolve outsidearchive_root. - Labels containing path separators can create unintended nested destinations.
The script only checks whether the resulting target already exists. It does not normalize the destination and verify that it remains beneath the resolved archive root. Consequently, a caller who controls command-line arguments can make the quarantine operation move a valid source to an arbitrary nonexistent destination permitted by the process's filesystem privileges.
Attack Path
-
The attacker or unsafe automation selects an existing source path that the process can move.
-
It invokes the script with an escaping label, for example:
bash python3 scripts/quarantine_path.py \ --source /path/to/source \
...[truncated 1244 chars]
- An absolute label causes
- Remediation
View remediation
Remediation Suggestions
Treat
--labelstrictly as one filesystem entry name rather than a path:- Reject empty labels, absolute paths,
.and... - Reject labels containing
/,\, or multiple path components. - Require
Path(args.label).name == args.label. - Resolve the archive root and proposed target, then explicitly enforce containment before moving.
- Perform the containment check before creating directories or modifying the source.
- Add tests for absolute labels, traversal sequences, mixed separators, and valid ordinary labels.
- Consider adding
--dry-runand explicit confirmation options for destructive moves.
Example hardening:
python label = Path(args.label) if ( not args.label or label.is_absolute() or args.label in {'.', '..'} or label.name != args.label or '/' in args.label or '\\' in args.label ): raise SystemExit('Label must be a single file or directory name') archive_root = Path(args.archive_root).resolve() target = (archive_root / args.label).resolve(strict=False) if not target.is_relative_to(archive_root): raise SystemExit('Target must remain inside the archive root') if target == archive_root: raise SystemExit('Target cannot be the archive root') if target.exists() or target.is_symlink(): raise SystemExit(f'Target already exists: {target}')The source may also be normalized and checked against policy-defined protected paths before
shutil.move()is called.- Reject empty labels, absolute paths,
