Opened 3 weeks ago

Last modified 2 weeks ago

#37390 assigned Bug

archive.extract() crashes with AttributeError on special tar members

Reported by: Dean Ruina Owned by: Dean Ruina
Component: Core (Management commands) Version: dev
Severity: Normal Keywords: startproject startapp archive hardening
Cc: Triage Stage: Accepted
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

django.utils.archive.extract() (used by startproject/startapp --template for tar/zip templates) crashes on a tar member that is a FIFO or a character/block device: TarFile.extractfile() returns None, and TarArchive.extract() then opens the target path for writing and calls shutil.copyfileobj(None, ...), which raises AttributeError - after an empty file has already been created at the destination. So a template archive containing a device/FIFO member aborts extraction uncleanly and leaves a partial tree behind, instead of failing with a clear error.

Steps to reproduce: build a tar whose members include a FIFO (e.g. a tarfile.TarInfo with type tarfile.FIFOTYPE), then run django-admin startproject proj --template that.tar. Expected: a clear error. Actual: AttributeError: 'NoneType' object has no attribute 'read' from shutil.copyfileobj in archive.py.

While fixing that, a related hardening gap in the same function: member modes are applied verbatim with os.chmod() (BaseArchive._copy_permissions), so a member with mode 0o4755 becomes a setuid file, and TemplateCommand.apply_umask() then copies stat.S_IMODE() -- including the setuid/setgid/sticky bits -- onto the generated project file.

Proposed fix, mirroring Python's own tarfile "data" filter (PEP 706, the default in Python 3.14):

  • raise SuspiciousOperation for tar members that are not regular files, directories or links, before anything is written (consistent with the function's existing handling of invalid paths);
  • mask the applied mode to 0o777 (the existing behavior of preserving 0o775 etc. is unchanged).

This hardening is in the spirit of CVE-2021-3281 and CVE-2025-59682, which both treated this function as a boundary against hostile templates. The documented "used as is" warning on --template is unchanged.
I have a patch ready with tests and a 6.2 release note, and will open a PR once the ticket is accepted.

Change History (3)

comment:1 by Dean Ruina, 3 weeks ago

Owner: set to Dean Ruina
Status: new → assigned

comment:2 by Sarah Boyce, 2 weeks ago

Summary: archive.extract() crashes with AttributeError on special tar members (and applies setuid/setgid bits) → archive.extract() crashes with AttributeError on special tar members
Triage Stage: Unreviewed → Accepted

Thank you for the report. I can confirm the FIFO crash. A tar member with type FIFOTYPE causes archive.extract() to create an empty file and then raise AttributeError:

  • tests/utils_tests/test_archive.py

    a b  
    11import os
    22import stat
    33import sys
     4import tarfile
    45import tempfile
    56import unittest
    67import zipfile
    … … class TestArchiveInvalid(SimpleTestCase):  
    113114                SuspiciousOperation, "Archive contains invalid path"
    114115            ):
    115116                archive.extract(tar_path, base)
     117
     118    def test_extract_rejects_fifo(self):
     119        with tempfile.TemporaryDirectory() as tmpdir:
     120            tar_path = os.path.join(tmpdir, "template.tar")
     121            destination = os.path.join(tmpdir, "output")
     122            os.mkdir(destination)
     123
     124            with tarfile.open(tar_path, "w") as tar:
     125                member = tarfile.TarInfo("template/fifo")
     126                member.type = tarfile.FIFOTYPE
     127                tar.addfile(member)
     128
     129            with self.assertRaises(AttributeError):  # SuspiciousOperation is better.
     130                archive.extract(tar_path, destination)
     131
     132            self.assertFalse(os.path.lexists(os.path.join(destination, "fifo")))

I agree that Django should reject unsupported tar members before creating a destination file. Raising SuspiciousOperation makes sense to me

I'm less certain about including the mode change in this fix. Could you open a separate ticket for it and explain the practical concern it addresses? That would let us assess the permission change on its own merits.

Note: See TracTickets for help on using tickets.
Back to Top