Description
Tar Slip uses crafted paths in a tar archive to write files outside the intended extraction directory. Absolute paths and relative paths such as ../ can allow important system files or sensitive data to be overwritten.
Potential impact
- File-system damage: An attacker may overwrite system files and disrupt the system.
- Information disclosure: Sensitive information may be exposed to an attacker.
- Privilege escalation: Modifying security-sensitive system files may allow an attacker to gain additional privileges.
Remediation
- Use the standard extraction filter: on supported Python versions, specify
extractall(..., filter="data")to restrict absolute paths, links outside the destination, special files, and excessive permissions. - Use a safe destination: extract into a new private directory under a trusted parent.
- Protect legacy runtimes: without extraction filters, check the resolved path against the directory boundary and reject symbolic links, hard links, device files, and other non-regular entries.
- Limit resources: extraction filters alone do not prevent archive bombs or excessive file counts. Limit total size, number of files, and processing time separately.
Examples
Before
python
# Tar extraction using the runtime default filter
import tarfile
def unsafe_extract(tar_file_path, extract_path):
with tarfile.open(tar_file_path) as tar:
tar.extractall(path=extract_path)
After
python
# Safe tar extraction
import tarfile
import os
import shutil
from pathlib import Path
def safe_extract(tar_file_path, extract_path):
destination = Path(extract_path)
# Extract only into a new private directory without existing files or links
destination.mkdir(mode=0o700, exist_ok=False)
destination = destination.resolve()
with tarfile.open(tar_file_path) as tar:
if hasattr(tarfile, "data_filter"):
# Restricted data extraction on runtimes that support extraction filters
tar.extractall(path=destination, filter="data")
return
# Legacy fallback: reject links and special files, and check directory boundaries
for member in tar.getmembers():
if member.issym() or member.islnk():
raise ValueError("Tar links are not allowed")
if not (member.isfile() or member.isdir()):
raise ValueError("Unsupported tar entry type")
target = (destination / member.name).resolve()
try:
target.relative_to(destination)
except ValueError as error:
raise ValueError("Tar entry escapes extraction directory") from error
if member.isdir():
target.mkdir(parents=True, exist_ok=True, mode=0o700)
continue
target.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
source = tar.extractfile(member)
if source is None:
raise ValueError("Tar entry has no file data")
fd = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with source, os.fdopen(fd, "wb") as output:
shutil.copyfileobj(source, output)
Explanation:
- Before: No extraction filter is specified. Python 3.14 uses
databy default, so this call alone does not mean paths outside the destination are allowed. Earlier versions or environments with a different default filter may permit writes through absolute paths or../. - After: The function creates a new private directory and explicitly uses the standard
datafilter where available. The legacy fallback checks each resolved path and rejects symbolic links, hard links, and special files, addressing link-based escapes that a simple..name check would miss.