From cbc3b3607fe3476d1eb4bc63c75c2a6b93430e32 Mon Sep 17 00:00:00 2001 From: Gustavo Date: Fri, 15 May 2026 21:09:15 -0300 Subject: [PATCH] Add B203: detect zipfile.extractall without path validation (Zip Slip) --- bandit/plugins/zipfile_unsafe_extractall.py | 91 +++++++++++++++++++++ examples/zipfile_extractall.py | 24 ++++++ setup.cfg | 1 + 3 files changed, 116 insertions(+) create mode 100644 bandit/plugins/zipfile_unsafe_extractall.py create mode 100644 examples/zipfile_extractall.py diff --git a/bandit/plugins/zipfile_unsafe_extractall.py b/bandit/plugins/zipfile_unsafe_extractall.py new file mode 100644 index 000000000..5fa60b304 --- /dev/null +++ b/bandit/plugins/zipfile_unsafe_extractall.py @@ -0,0 +1,91 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# +r""" +====================================== +B203: Test for zipfile.extractall +====================================== + +This plugin detects usage of ``zipfile.ZipFile.extractall()`` without +path validation, which may allow Zip Slip attacks (CWE-22). + +An attacker can craft a ZIP archive with entries containing path traversal +sequences (e.g. ``../../etc/cron.d/evil``) that, when extracted without +validation, write files outside the intended destination directory. + +Severity is set as follows: + +* ``zipfile.extractall()`` with no validation - HIGH +* ``zipfile.extractall(path=?)`` with a variable path but no member check - MEDIUM + +Use a manual extraction loop that validates each member path before +extracting: + +.. code-block:: python + + import zipfile, os + + def safe_extract(zf, dest): + dest = os.path.realpath(dest) + for member in zf.namelist(): + member_path = os.path.realpath(os.path.join(dest, member)) + if not member_path.startswith(dest + os.sep): + raise ValueError("Zip Slip detected: %s" % member) + zf.extract(member, dest) + +:Example: + +.. code-block:: none + + >> Issue: [B203:zipfile_unsafe_extractall] zipfile.extractall used + without path validation. Possible Zip Slip (path traversal) attack. + Severity: High Confidence: High + CWE: CWE-22 (https://cwe.mitre.org/data/definitions/22.html) + Location: examples/zipfile_extractall.py:5 + +.. seealso:: + + - https://docs.python.org/3/library/zipfile.html#zipfile.ZipFile.extractall + - https://security.snyk.io/research/zip-slip-vulnerability + - https://cwe.mitre.org/data/definitions/22.html + +.. versionadded:: 1.8.0 + +""" +import bandit +from bandit.core import issue +from bandit.core import test_properties as test + + +@test.test_id("B203") +@test.checks("Call") +def zipfile_unsafe_extractall(context): + if all( + [ + context.is_module_imported_exact("zipfile"), + "extractall" in context.call_function_name, + ] + ): + if "members" in context.call_keywords: + return bandit.Issue( + severity=bandit.MEDIUM, + confidence=bandit.MEDIUM, + cwe=issue.Cwe.PATH_TRAVERSAL, + text=( + "zipfile.extractall used with a members argument but " + "without verified path validation. Ensure each member " + "path is checked against the destination directory to " + "prevent Zip Slip attacks (CWE-22)." + ), + ) + return bandit.Issue( + severity=bandit.HIGH, + confidence=bandit.HIGH, + cwe=issue.Cwe.PATH_TRAVERSAL, + text=( + "zipfile.extractall used without path validation. " + "A crafted ZIP archive may extract files outside the " + "intended directory (Zip Slip, CWE-22). Use a manual " + "extraction loop with path validation instead." + ), + ) diff --git a/examples/zipfile_extractall.py b/examples/zipfile_extractall.py new file mode 100644 index 000000000..78c14c156 --- /dev/null +++ b/examples/zipfile_extractall.py @@ -0,0 +1,24 @@ +import zipfile +import tempfile +import os + +# B203: HIGH — sem validação nenhuma +with zipfile.ZipFile("archive.zip") as zf: + zf.extractall(tempfile.mkdtemp()) + +# B203: HIGH — path especificado mas sem validar membros +dest = "/tmp/output" +with zipfile.ZipFile("archive.zip") as zf: + zf.extractall(path=dest) + +# B203: MEDIUM — members passado mas sem validação de path confirmada +with zipfile.ZipFile("archive.zip") as zf: + zf.extractall(path=dest, members=zf.namelist()) + +def safe_extract(zf, destination): + destination = os.path.realpath(destination) + for member in zf.namelist(): + member_path = os.path.realpath(os.path.join(destination, member)) + if not member_path.startswith(destination + os.sep): + raise ValueError("Zip Slip detected: %s" % member) + zf.extract(member, destination) diff --git a/setup.cfg b/setup.cfg index fe4c74746..9088e3dae 100644 --- a/setup.cfg +++ b/setup.cfg @@ -153,6 +153,7 @@ bandit.plugins = #bandit/plugins/tarfile_unsafe_members.py tarfile_unsafe_members = bandit.plugins.tarfile_unsafe_members:tarfile_unsafe_members + zipfile_unsafe_extractall = bandit.plugins.zipfile_unsafe_extractall:zipfile_unsafe_extractall #bandit/plugins/pytorch_load.py pytorch_load = bandit.plugins.pytorch_load:pytorch_load