v2.2.2
This commit is contained in:
parent
f3e39fb8e6
commit
e5f5d32aab
124 changed files with 11892 additions and 2461 deletions
156
tests/test_path_traversal.sh
Executable file
156
tests/test_path_traversal.sh
Executable file
|
|
@ -0,0 +1,156 @@
|
|||
#!/bin/bash
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
# Path traversal / Zip Slip regression tests.
|
||||
#
|
||||
# Verifies that the v2.2.2 audit fixes for CVE-pattern path traversal
|
||||
# (Snyk Zip Slip 2018) and symlink-following on extract are working.
|
||||
#
|
||||
# Tests construct malicious archives in two ways:
|
||||
# (A) compress with a relative path then post-mutate the index (manual fuzz)
|
||||
# (B) try to extract into a directory containing a symlink with the same
|
||||
# name as an archive entry — should be refused due to O_NOFOLLOW.
|
||||
|
||||
ZUPT_BIN="$(realpath ./zupt)"
|
||||
TMPDIR=$(mktemp -d)
|
||||
trap "rm -rf $TMPDIR" EXIT
|
||||
cd "$TMPDIR"
|
||||
|
||||
PASS=0; FAIL=0
|
||||
chk() {
|
||||
if [ $? -eq 0 ]; then echo " ✓ $1"; PASS=$((PASS+1))
|
||||
else echo " ✗ $1"; FAIL=$((FAIL+1)); fi
|
||||
}
|
||||
|
||||
# ─── Property 1: archive with ".." entry must not extract above target ──
|
||||
# Strategy: compress an innocent file, then patch the archive's index to
|
||||
# replace the path with "../../escaped.txt". Extract into a subdir;
|
||||
# verify the file appears nowhere outside the subdir.
|
||||
echo " [P1. Zip Slip — relative path traversal blocked]"
|
||||
|
||||
mkdir input output_safe
|
||||
echo "secret content" > input/innocent.txt
|
||||
"$ZUPT_BIN" c slip.zupt input/innocent.txt > /dev/null 2>&1
|
||||
|
||||
# Patch the archive: replace "input/innocent.txt" path string with
|
||||
# "../escape.txt" in the index. We use a python helper because the index
|
||||
# is varint-prefixed and we need to keep length consistent.
|
||||
python3 << 'PYEOF'
|
||||
import sys
|
||||
data = bytearray(open('slip.zupt','rb').read())
|
||||
target = b'input/innocent.txt'
|
||||
replacement = b'../escape.txt'
|
||||
# Pad replacement to same length so varint length prefix stays valid
|
||||
pad = b'\x00' * (len(target) - len(replacement))
|
||||
i = data.find(target)
|
||||
if i < 0:
|
||||
print("ERROR: pattern not in archive")
|
||||
sys.exit(1)
|
||||
# Replace the bytes — note this will fail validation below, which is OK,
|
||||
# we want to see if extract REJECTS the malformed path.
|
||||
data[i:i+len(target)] = replacement + pad
|
||||
open('slip_patched.zupt','wb').write(bytes(data))
|
||||
PYEOF
|
||||
|
||||
# Try to extract — even if the patched archive is corrupt, we want to
|
||||
# verify that NO file appears at "../escape.txt" relative to output_safe.
|
||||
cd output_safe
|
||||
"$ZUPT_BIN" x ../slip_patched.zupt > /dev/null 2>&1
|
||||
cd ..
|
||||
|
||||
# The key invariant: nothing escaped to TMPDIR (parent of output_safe)
|
||||
[ ! -f "$TMPDIR/escape.txt" ] && [ ! -f escape.txt ]
|
||||
chk "No escape via patched ../escape.txt path"
|
||||
|
||||
# ─── Property 2: archive with absolute path must not write to that path ──
|
||||
echo " [P2. Absolute path entries blocked]"
|
||||
|
||||
# Construct an archive entry with absolute "/tmp/owned.txt" via patching
|
||||
echo "innocent" > input2.txt
|
||||
"$ZUPT_BIN" c abs.zupt input2.txt > /dev/null 2>&1
|
||||
python3 << 'PYEOF'
|
||||
data = bytearray(open('abs.zupt','rb').read())
|
||||
target = b'input2.txt'
|
||||
# Replace with absolute path of equal length
|
||||
replacement = b'/tmp/owned' # 10 chars vs 10 chars
|
||||
i = data.find(target)
|
||||
if i >= 0:
|
||||
data[i:i+len(target)] = replacement
|
||||
open('abs_patched.zupt','wb').write(bytes(data))
|
||||
PYEOF
|
||||
|
||||
mkdir abs_extract
|
||||
cd abs_extract
|
||||
"$ZUPT_BIN" x ../abs_patched.zupt > /dev/null 2>&1
|
||||
cd ..
|
||||
|
||||
[ ! -f /tmp/owned ]
|
||||
chk "Absolute /tmp/owned path rejected"
|
||||
|
||||
# ─── Property 3: symlink at output target is not followed ──────────────
|
||||
# Pre-place a symlink in output dir pointing to a sentinel file.
|
||||
# Extract an archive with the same entry name; verify the sentinel is
|
||||
# unchanged (i.e. extract refused to follow the symlink).
|
||||
echo " [P3. Symlink at extract target not followed]"
|
||||
|
||||
echo "DO_NOT_OVERWRITE" > sentinel.txt
|
||||
mkdir symlink_extract
|
||||
ln -s "$(pwd)/sentinel.txt" symlink_extract/innocent.txt
|
||||
|
||||
# Build a fresh non-patched archive with "innocent.txt"
|
||||
mkdir input3 && echo "evil overwrite content" > input3/innocent.txt
|
||||
"$ZUPT_BIN" c clean.zupt input3/innocent.txt > /dev/null 2>&1
|
||||
# Mutate path "input3/innocent.txt" -> "innocent.txt" so it lands at the symlink
|
||||
python3 << 'PYEOF'
|
||||
data = bytearray(open('clean.zupt','rb').read())
|
||||
target = b'input3/innocent.txt'
|
||||
replacement = b'innocent.txt' + (b'\x00' * (len(target) - len(b'innocent.txt')))
|
||||
i = data.find(target)
|
||||
if i >= 0:
|
||||
data[i:i+len(target)] = replacement
|
||||
open('clean_patched.zupt','wb').write(bytes(data))
|
||||
PYEOF
|
||||
|
||||
cd symlink_extract
|
||||
"$ZUPT_BIN" x ../clean_patched.zupt > /dev/null 2>&1
|
||||
cd ..
|
||||
|
||||
# Sentinel must be unchanged — symlink follow would have overwritten it
|
||||
content=$(cat sentinel.txt)
|
||||
[ "$content" = "DO_NOT_OVERWRITE" ]
|
||||
chk "Sentinel via symlink not overwritten"
|
||||
|
||||
# ─── Property 4: legitimate paths still extract correctly ─────────────
|
||||
echo " [P4. Legitimate (safe) paths still extract]"
|
||||
|
||||
mkdir legit_input
|
||||
echo "ok content" > legit_input/normal.txt
|
||||
"$ZUPT_BIN" c legit.zupt legit_input/normal.txt > /dev/null 2>&1
|
||||
|
||||
mkdir legit_extract && cd legit_extract
|
||||
"$ZUPT_BIN" x ../legit.zupt > /dev/null 2>&1
|
||||
cd ..
|
||||
|
||||
[ -f legit_extract/legit_input/normal.txt ] && \
|
||||
[ "$(cat legit_extract/legit_input/normal.txt)" = "ok content" ]
|
||||
chk "Normal extraction still works"
|
||||
|
||||
# ─── Property 5: deep path (allowed) but parent dir is created ─────────
|
||||
echo " [P5. Multi-component safe paths still work]"
|
||||
|
||||
mkdir deep && mkdir deep/sub && mkdir deep/sub/sub2
|
||||
echo "deep" > deep/sub/sub2/file.txt
|
||||
"$ZUPT_BIN" c deep.zupt deep/sub/sub2/file.txt > /dev/null 2>&1
|
||||
|
||||
mkdir deep_extract && cd deep_extract
|
||||
"$ZUPT_BIN" x ../deep.zupt > /dev/null 2>&1
|
||||
cd ..
|
||||
|
||||
[ -f deep_extract/deep/sub/sub2/file.txt ]
|
||||
chk "Deep nested path extracted"
|
||||
|
||||
echo
|
||||
echo " ───────────────────────────────────────"
|
||||
echo " Path-traversal regression: $PASS passed, $FAIL failed"
|
||||
echo " ───────────────────────────────────────"
|
||||
[ $FAIL -eq 0 ]
|
||||
Loading…
Reference in a new issue