Files
wehub-resource-sync 7a0da7932b
Backwards Compatibility / Verify Encryption Constants (push) Waiting to run
Backwards Compatibility / PyPI Version Compatibility (push) Waiting to run
Backwards Compatibility / Database Migration Tests (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Blocked by required conditions
Docker Tests (Consolidated) / detect-changes (push) Waiting to run
Docker Tests (Consolidated) / Build Test Image (push) Waiting to run
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Blocked by required conditions
Docker Tests (Consolidated) / Accessibility Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Unit Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Example Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / Production Image Smoke Test (push) Blocked by required conditions
Docker Tests (Consolidated) / Infrastructure Tests (push) Blocked by required conditions
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Waiting to run
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

283 lines
11 KiB
Python

"""Coverage tests for security/path_validator.py.
Focuses on adversarial inputs and uncovered branches:
- Null bytes in all methods
- Path traversal attempts (../)
- URL-encoded traversal
- validate_local_filesystem_path: control chars, ~ expansion,
restricted dirs, relative paths
- validate_safe_path: extension filtering, None result
- sanitize_for_filesystem_ops: non-absolute path
- validate_config_path: restricted prefixes, absolute paths,
missing config files
- validate_model_path: missing/non-file model
- validate_data_path success/failure
"""
from pathlib import Path
from unittest.mock import patch
import pytest
from local_deep_research.security.path_validator import PathValidator
MODULE = "local_deep_research.security.path_validator"
# ---------------------------------------------------------------------------
# validate_safe_path
# ---------------------------------------------------------------------------
class TestValidateSafePath:
def test_null_byte_raises(self):
with pytest.raises(ValueError, match="Null bytes"):
PathValidator.validate_safe_path("file\x00name.txt", "/tmp")
def test_empty_string_raises(self):
with pytest.raises(ValueError, match="Invalid path input"):
PathValidator.validate_safe_path("", "/tmp")
def test_none_input_raises(self):
with pytest.raises(ValueError, match="Invalid path input"):
PathValidator.validate_safe_path(None, "/tmp")
def test_path_traversal_blocked(self):
with pytest.raises(ValueError, match="traversal|Invalid path"):
PathValidator.validate_safe_path("../etc/passwd", "/tmp")
def test_double_dot_deeply_nested_allowed_by_werkzeug(self):
"""werkzeug safe_join resolves a/b/../../etc/passwd within the base, so it's
accepted (it resolves to base/etc/passwd, not an escape). No exception expected."""
result = PathValidator.validate_safe_path(
"a/b/../../c/file.txt", "/tmp"
)
assert result is not None
def test_valid_relative_path_accepted(self, tmp_path):
result = PathValidator.validate_safe_path(
"subdir/file.txt", str(tmp_path)
)
assert result is not None
assert "subdir/file.txt" in str(result)
def test_wrong_extension_raises(self, tmp_path):
with pytest.raises(ValueError, match="Invalid file type"):
PathValidator.validate_safe_path(
"config.txt",
str(tmp_path),
required_extensions=(".json", ".yaml"),
)
def test_correct_extension_accepted(self, tmp_path):
result = PathValidator.validate_safe_path(
"config.json",
str(tmp_path),
required_extensions=(".json", ".yaml"),
)
assert result.suffix == ".json"
def test_whitespace_stripped_from_input(self, tmp_path):
result = PathValidator.validate_safe_path(" file.txt ", str(tmp_path))
assert result is not None
# ---------------------------------------------------------------------------
# validate_local_filesystem_path
# ---------------------------------------------------------------------------
class TestValidateLocalFilesystemPath:
def test_null_byte_raises(self):
with pytest.raises(ValueError, match="Null bytes"):
PathValidator.validate_local_filesystem_path("/tmp/file\x00.txt")
def test_control_characters_raise(self):
with pytest.raises(ValueError, match="Control characters"):
PathValidator.validate_local_filesystem_path("/tmp/file\x01.txt")
def test_double_dot_traversal_blocked(self):
with pytest.raises(ValueError, match="traversal"):
PathValidator.validate_local_filesystem_path("/tmp/../etc/passwd")
def test_tilde_expansion(self, tmp_path):
"""~ is expanded to home directory."""
with patch.object(Path, "home", return_value=tmp_path):
result = PathValidator.validate_local_filesystem_path("~/subdir")
assert str(tmp_path) in str(result)
def test_tilde_only_expansion(self, tmp_path):
"""~ alone expands to home directory."""
with patch.object(Path, "home", return_value=tmp_path):
result = PathValidator.validate_local_filesystem_path("~")
assert str(tmp_path) in str(result)
def test_restricted_etc_blocked(self):
with pytest.raises(ValueError, match="system directories"):
PathValidator.validate_local_filesystem_path("/etc/passwd")
def test_restricted_proc_blocked(self):
with pytest.raises(ValueError, match="system directories"):
PathValidator.validate_local_filesystem_path("/proc/cpuinfo")
def test_restricted_sys_blocked(self):
with pytest.raises(ValueError, match="system directories"):
PathValidator.validate_local_filesystem_path("/sys/kernel")
def test_restricted_dev_blocked(self):
with pytest.raises(ValueError, match="system directories"):
PathValidator.validate_local_filesystem_path("/dev/null")
def test_home_directory_allowed(self, tmp_path):
"""User home dir is not restricted."""
result = PathValidator.validate_local_filesystem_path(str(tmp_path))
assert result is not None
def test_empty_string_raises(self):
with pytest.raises(ValueError, match="Invalid path input"):
PathValidator.validate_local_filesystem_path("")
def test_none_raises(self):
with pytest.raises(ValueError, match="Invalid path input"):
PathValidator.validate_local_filesystem_path(None)
def test_relative_path_resolves(self, tmp_path):
"""Relative paths are resolved against cwd."""
with patch("os.getcwd", return_value=str(tmp_path)):
result = PathValidator.validate_local_filesystem_path("subdir")
assert result is not None
def test_custom_restricted_dirs(self, tmp_path):
"""Custom restricted_dirs are respected."""
restricted = tmp_path / "secret"
restricted.mkdir()
with pytest.raises(ValueError, match="system directories"):
PathValidator.validate_local_filesystem_path(
str(restricted), restricted_dirs=[restricted]
)
# ---------------------------------------------------------------------------
# sanitize_for_filesystem_ops
# ---------------------------------------------------------------------------
class TestSanitizeForFilesystemOps:
def test_non_absolute_path_raises(self):
with pytest.raises(ValueError, match="must be absolute"):
PathValidator.sanitize_for_filesystem_ops(Path("relative/path"))
def test_absolute_path_returned(self, tmp_path):
result = PathValidator.sanitize_for_filesystem_ops(tmp_path)
assert result.is_absolute()
assert str(tmp_path) in str(result)
# ---------------------------------------------------------------------------
# validate_config_path
# ---------------------------------------------------------------------------
class TestValidateConfigPath:
def test_null_byte_raises(self):
with pytest.raises(ValueError, match="Null bytes"):
PathValidator.validate_config_path("config\x00.json")
def test_empty_string_raises(self):
with pytest.raises(ValueError, match="Invalid config path input"):
PathValidator.validate_config_path("")
def test_none_raises(self):
with pytest.raises(ValueError, match="Invalid config path input"):
PathValidator.validate_config_path(None)
def test_double_dot_traversal_blocked(self):
with pytest.raises(ValueError, match="traversal"):
PathValidator.validate_config_path("../../etc/passwd.json")
def test_restricted_etc_prefix_blocked(self):
with pytest.raises(ValueError, match="restricted system directory"):
PathValidator.validate_config_path("/etc/shadow.json")
def test_restricted_proc_prefix_blocked(self):
with pytest.raises(ValueError, match="restricted system directory"):
PathValidator.validate_config_path("proc/something")
def test_absolute_path_invalid_extension_blocked(self):
"""Absolute path starting with / causes safe_join(/, path) -> None -> error."""
# safe_join("/", "/some/path") returns None for any absolute path
# so any absolute /path causes "Invalid absolute path" or a traversal error
with pytest.raises(ValueError):
PathValidator.validate_config_path("/some/config.exe")
def test_absolute_path_causes_safe_join_failure(self):
"""validate_config_path with /absolute/path raises ValueError since
safe_join('/','/'+ path) returns None (werkzeug treats it as traversal)."""
with pytest.raises(ValueError):
PathValidator.validate_config_path("/home/user/config.json")
def test_relative_path_with_valid_extension_accepted(self, tmp_path):
"""Relative config path with valid extension goes through validate_safe_path."""
# Create a json file inside tmp_path so the path is valid
config_file = tmp_path / "my_config.json"
config_file.write_text("{}")
result = PathValidator.validate_config_path(
"my_config.json", config_root=str(tmp_path)
)
assert result.suffix == ".json"
# ---------------------------------------------------------------------------
# validate_model_path
# ---------------------------------------------------------------------------
class TestValidateModelPath:
def test_missing_model_file_raises(self, tmp_path):
with pytest.raises(ValueError, match="not found"):
PathValidator.validate_model_path(
"nonexistent.gguf", model_root=str(tmp_path)
)
def test_path_is_directory_not_file_raises(self, tmp_path):
subdir = tmp_path / "models"
subdir.mkdir()
with pytest.raises(ValueError, match="not a file|not found"):
PathValidator.validate_model_path(
"models", model_root=str(tmp_path)
)
def test_valid_model_file_accepted(self, tmp_path):
model = tmp_path / "model.gguf"
model.write_bytes(b"fake gguf data")
result = PathValidator.validate_model_path(
"model.gguf", model_root=str(tmp_path)
)
assert result == model
def test_path_traversal_in_model_path_blocked(self, tmp_path):
with pytest.raises(ValueError, match="traversal|Invalid path"):
PathValidator.validate_model_path(
"../etc/passwd", model_root=str(tmp_path)
)
# ---------------------------------------------------------------------------
# validate_data_path
# ---------------------------------------------------------------------------
class TestValidateDataPath:
def test_traversal_blocked(self, tmp_path):
with pytest.raises(ValueError, match="traversal|Invalid path"):
PathValidator.validate_data_path("../../secret", str(tmp_path))
def test_valid_data_path_accepted(self, tmp_path):
result = PathValidator.validate_data_path("data/file.db", str(tmp_path))
assert result is not None
assert "data/file.db" in str(result)
def test_null_byte_blocked(self, tmp_path):
with pytest.raises(ValueError, match="Null bytes|Invalid path"):
PathValidator.validate_data_path("file\x00.db", str(tmp_path))