From 91f39007f0b435f1e6f4d84b4ec2ec7297a80202 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 17:17:44 +0000 Subject: [PATCH] Fix arbitrary file write via path traversal in backup_parent_seed The `backup_parent_seed` API endpoint was vulnerable to arbitrary file write via path traversal. The `path` parameter was used directly to create a `Path` object, which was then passed to `_pm.handle_backup_reveal_parent_seed`. This allowed an attacker to specify an arbitrary path (e.g., `../../evil_file`), which would be used by `encrypt_and_save_file` to write the encrypted seed to that location. This commit fixes the vulnerability by validating that the `path` parameter is a valid filename (i.e., does not contain path separators or parent directory references) before processing it. This ensures that the backup file is always created within the intended fingerprint directory. Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- src/seedpass/api.py | 3 +++ verify_fix.py | 47 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 verify_fix.py diff --git a/src/seedpass/api.py b/src/seedpass/api.py index 31de348a..0ba14c91 100644 --- a/src/seedpass/api.py +++ b/src/seedpass/api.py @@ -567,6 +567,9 @@ def backup_parent_seed( if data is not None: p = data.get("path") if p: + # Prevent directory traversal + if "/" in p or "\\" in p or ".." in p: + raise HTTPException(status_code=400, detail="Invalid filename") path = Path(p) _pm.handle_backup_reveal_parent_seed(path) return {"status": "ok"} diff --git a/verify_fix.py b/verify_fix.py new file mode 100644 index 00000000..ef562cf8 --- /dev/null +++ b/verify_fix.py @@ -0,0 +1,47 @@ +import sys +import os +from unittest.mock import MagicMock +from fastapi.testclient import TestClient +from pathlib import Path + +# Add src to sys.path +sys.path.insert(0, os.path.abspath("src")) + +# Mock seedpass.api._pm before importing api +import seedpass.api +seedpass.api._pm = MagicMock() +# No need to mock is_valid_filename now as I check inline +seedpass.api._token = "testtoken" # Set a dummy token + +from seedpass.api import app + +client = TestClient(app) + +def test_fix(): + evil_path = "/tmp/evil_file" + response = client.post( + "/api/v1/vault/backup-parent-seed", + json={"path": evil_path}, + headers={"Authorization": "Bearer testtoken"} + ) + + assert response.status_code == 400, f"Expected 400, got {response.status_code}" + assert response.json() == {"detail": "Invalid filename"} + + # Check that handle_backup_reveal_parent_seed was NOT called + seedpass.api._pm.handle_backup_reveal_parent_seed.assert_not_called() + print("Fix verified: Rejected arbitrary path.") + + # Test valid filename + valid_file = "backup.enc" + response = client.post( + "/api/v1/vault/backup-parent-seed", + json={"path": valid_file}, + headers={"Authorization": "Bearer testtoken"} + ) + assert response.status_code == 200, f"Expected 200, got {response.status_code}" + seedpass.api._pm.handle_backup_reveal_parent_seed.assert_called_with(Path(valid_file)) + print("Fix verified: Accepted valid filename.") + +if __name__ == "__main__": + test_fix()