diff --git a/test/test_invariant_dump_win_syms.py b/test/test_invariant_dump_win_syms.py new file mode 100644 index 0000000000..301abbed50 --- /dev/null +++ b/test/test_invariant_dump_win_syms.py @@ -0,0 +1,53 @@ +import pytest +import os +import sys +import tempfile +from pathlib import Path + +# Import the actual module +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +from tools.dump_win_syms import main + + +@pytest.mark.parametrize("payload", [ + "../../../etc/passwd", # Classic path traversal + "....//....//etc/passwd", # Double-encoded traversal + "%2e%2e%2fetc%2fpasswd", # URL-encoded traversal + "subdir/file.txt", # Valid relative path within root + "file.txt", # Valid filename +]) +def test_path_traversal_blocked(payload, tmp_path, monkeypatch, capsys): + """Invariant: File operations never resolve paths outside the declared root directory""" + + # Create a temporary root directory for file operations + root_dir = tmp_path / "root" + root_dir.mkdir() + + # Create a test file inside root + test_file = root_dir / "file.txt" + test_file.write_text("test content") + + # Create a sensitive file outside root (should not be accessible) + outside_file = tmp_path / "sensitive.txt" + outside_file.write_text("secret data") + + # Mock sys.argv to pass the payload as argument + monkeypatch.setattr(sys, 'argv', ['dump_win_syms.py', str(root_dir), payload]) + + # Attempt to access the file + try: + main() + except (SystemExit, FileNotFoundError, ValueError, OSError) as e: + # Expected: traversal attempts should fail or be rejected + pass + + # Verify that sensitive file outside root was never read + captured = capsys.readouterr() + assert "secret data" not in captured.out, \ + f"Path traversal payload '{payload}' allowed access outside root directory" + + # For valid payloads, verify they resolve within root + if not payload.startswith(("../", "....//", "%2e")): + resolved = (root_dir / payload).resolve() + assert str(resolved).startswith(str(root_dir.resolve())), \ + f"Valid path '{payload}' resolved outside root directory" \ No newline at end of file diff --git a/tools/dump_win_syms.py b/tools/dump_win_syms.py index 009feed44d..6de10bdb2f 100644 --- a/tools/dump_win_syms.py +++ b/tools/dump_win_syms.py @@ -1,6 +1,18 @@ import sys, os import subprocess +def safe_open(path, mode): + norm = os.path.normpath(path) + if any(part == '..' for part in norm.split(os.sep)): + raise ValueError("Path traversal detected: " + path) + return open(norm, mode) + +def safe_call(cmd, **kwargs): + for arg in cmd: + if isinstance(arg, str) and any(p == '..' for p in os.path.normpath(arg).split(os.sep)): + raise ValueError("Path traversal in subprocess arg: " + arg) + return subprocess.call(cmd, **kwargs) + nw_exe = os.path.normpath(sys.argv[1]) nw_dll = os.path.normpath(sys.argv[2]) node_dll = os.path.normpath(sys.argv[3]) @@ -9,8 +21,8 @@ dll_sym_file = nw_dll + ".sym" node_sym_file = node_dll + ".sym" dump_exe = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'dump_syms.exe') -subprocess.call([dump_exe, nw_exe], stdout=open(sym_file, 'w')) -subprocess.call([dump_exe, nw_dll], stdout=open(dll_sym_file, 'w')) -subprocess.call([dump_exe, node_dll], stdout=open(node_sym_file, 'w')) +safe_call([dump_exe, nw_exe], stdout=safe_open(sym_file, 'w')) +safe_call([dump_exe, nw_dll], stdout=safe_open(dll_sym_file, 'w')) +safe_call([dump_exe, node_dll], stdout=safe_open(node_sym_file, 'w')) lzma_exe = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', '..', '..', 'third_party', 'lzma_sdk', 'bin', 'win64', '7za.exe') -subprocess.call([lzma_exe, 'a', '-t7z', out_file, sym_file, dll_sym_file, node_sym_file]) +safe_call([lzma_exe, 'a', '-t7z', out_file, sym_file, dll_sym_file, node_sym_file])