T09 · Insecure Skill Coding Practices
- Location
scripts/download_arxiv_source.py:286- Finding
Unsafe Tar Archive Extraction Allows Writes Outside the Destination Directory
- Content
View full analysis
Vulnerability Details
File Location:
scripts/download_arxiv_source.py, lines 286–292
Vulnerability Type: Unsafe archive extraction and incomplete path-containment validation
Risk Level: MediumVulnerable Code
python def extract_tar_safely(archive: tarfile.TarFile, dest_dir: Path) -> None: root = dest_dir.resolve() for member in archive.getmembers(): target = (dest_dir / member.name).resolve() if not str(target).startswith(str(root)): raise ValueError(f"Unsafe path in tar: {member.name}") archive.extractall(dest_dir)Technical Analysis
The function attempts to prevent tar path traversal by resolving each archive member's destination and comparing path strings with
startswith(). String-prefix comparison does not establish that one filesystem path is a descendant of another.For example, if
rootis/run/source_extract, a malicious member resolving to/run/source_extract_backup/payloadpasses the check because that path begins with the string/run/source_extract, even though it is outside the intended directory.The validation also does not reject symbolic links, hard links, device files, or FIFOs, nor does it validate archive link targets. Consequently, an archive may use special member types or link-based paths to redirect subsequent extraction writes outside the destination. Calling
archive.extractall(dest_dir)after this incomplete validation exposes the filesystem to attacker-controlled archive semantics.Attack Path
- An attacker causes the processor to download or reuse a crafted source bundle for a paper.
- The bundle contains a tar member whose resolved path has a prefix collision with the extraction root, or contains a symbolic or hard link targeting a location outside that root.
extract_from_blob()opens the untrusted payload as a tar archive and callsextract_tar_safely().- The string-prefix check accepts the prefix-collision path, or the function ...[truncated 950 chars]
- Remediation
View remediation
Remediation Suggestions
- Replace string-prefix comparison with a path-aware containment test:
python root = dest_dir.resolve() target = (root / member.name).resolve() if not target.is_relative_to(root): raise ValueError(f"Unsafe path in tar: {member.name}")For Python versions without
Path.is_relative_to(), usetarget.relative_to(root)and reject members when it raisesValueError.- Reject unsafe member types unless they are explicitly required:
python if member.issym() or member.islnk() or member.isdev() or member.isfifo(): raise ValueError(f"Unsupported tar member type: {member.name}")- On supported Python versions, use the standard data extraction filter:
python archive.extractall(dest_dir, filter="data")This should supplement, rather than replace, explicit application-specific validation.
-
Extract into a newly created temporary directory on the same filesystem. Validate all extracted paths and object types before atomically moving the directory into its final location.
-
Avoid extracting over an existing directory. Remove or quarantine stale extraction output first so preexisting symbolic links cannot influence writes.
-
Add regression tests covering:
../traversal entries;- absolute paths;
- prefix-collision paths such as
../source_extract_backup/file; - symbolic links pointing outside the extraction root;
- hard links targeting outside paths;
- device files and FIFOs;
- nested link chains.
