From e6f53862f5aaef1234346612083609f3b0983c56 Mon Sep 17 00:00:00 2001 From: Sebastion Date: Wed, 20 May 2026 22:55:17 +0100 Subject: [PATCH] fix(web): sanitize username in report file paths to prevent path traversal (#2678) * fix: sanitize username in report file paths to prevent path traversal (CWE-22) * address review: add unit tests for sanitize_username_for_path() --- maigret/web/app.py | 21 ++++++++++++++++----- tests/test_sanitize_username.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 tests/test_sanitize_username.py diff --git a/maigret/web/app.py b/maigret/web/app.py index 864fa82..a0b8fa3 100644 --- a/maigret/web/app.py +++ b/maigret/web/app.py @@ -99,6 +99,16 @@ async def search_multiple_usernames(usernames, options): return results +def sanitize_username_for_path(username: str) -> str: + """Remove path separators and dangerous components from username for safe file path usage.""" + # Replace path separators and null bytes + sanitized = username.replace('/', '_').replace('\\', '_').replace('\0', '_') + # Remove . and .. components + sanitized = sanitized.strip('.') + # If empty after sanitization, use a fallback + return sanitized or '_' + + def process_search_task(usernames, options, timestamp): try: loop = asyncio.new_event_loop() @@ -123,7 +133,8 @@ def process_search_task(usernames, options, timestamp): individual_reports = [] for username, id_type, results in general_results: - report_base = os.path.join(session_folder, f"report_{username}") + safe_username = sanitize_username_for_path(username) + report_base = os.path.join(session_folder, f"report_{safe_username}") csv_path = f"{report_base}.csv" json_path = f"{report_base}.json" @@ -162,16 +173,16 @@ def process_search_task(usernames, options, timestamp): { 'username': username, 'csv_file': os.path.join( - f"search_{timestamp}", f"report_{username}.csv" + f"search_{timestamp}", f"report_{safe_username}.csv" ), 'json_file': os.path.join( - f"search_{timestamp}", f"report_{username}.json" + f"search_{timestamp}", f"report_{safe_username}.json" ), 'pdf_file': os.path.join( - f"search_{timestamp}", f"report_{username}.pdf" + f"search_{timestamp}", f"report_{safe_username}.pdf" ), 'html_file': os.path.join( - f"search_{timestamp}", f"report_{username}.html" + f"search_{timestamp}", f"report_{safe_username}.html" ), 'claimed_profiles': claimed_profiles, } diff --git a/tests/test_sanitize_username.py b/tests/test_sanitize_username.py new file mode 100644 index 0000000..ba0b20f --- /dev/null +++ b/tests/test_sanitize_username.py @@ -0,0 +1,28 @@ +import pytest + +from maigret.web.app import sanitize_username_for_path + + +@pytest.mark.parametrize( + "input_username, expected", + [ + ("../../tmp/x", "_.._tmp_x"), + ("..", "_"), + ("....", "_"), + ("foo/bar", "foo_bar"), + ("\0foo", "_foo"), + ("normaluser123", "normaluser123"), + ], +) +def test_sanitize_username_for_path(input_username, expected): + result = sanitize_username_for_path(input_username) + assert result == expected + # Verify no path separators or null bytes remain + assert "/" not in result + assert "\\" not in result + assert "\0" not in result + # Verify result is not empty + assert len(result) > 0 + # Verify no leading/trailing dots + assert not result.startswith(".") + assert not result.endswith(".")