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
277 lines
10 KiB
Python
277 lines
10 KiB
Python
"""High-value tests for security/path_validator.py - Path Validation."""
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from local_deep_research.security.path_validator import PathValidator
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# validate_safe_path()
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestValidateSafePath:
|
|
"""Safe path validation with base directory containment."""
|
|
|
|
def test_null_bytes_rejected(self, tmp_path):
|
|
with pytest.raises(ValueError, match="[Nn]ull"):
|
|
PathValidator.validate_safe_path("file\x00.txt", tmp_path)
|
|
|
|
def test_traversal_blocked(self, tmp_path):
|
|
with pytest.raises(ValueError, match="traversal"):
|
|
PathValidator.validate_safe_path("../../etc/passwd", tmp_path)
|
|
|
|
def test_valid_relative_path(self, tmp_path):
|
|
result = PathValidator.validate_safe_path("subdir/file.txt", tmp_path)
|
|
assert result is not None
|
|
assert str(tmp_path) in str(result)
|
|
|
|
def test_extension_filtering_valid(self, tmp_path):
|
|
result = PathValidator.validate_safe_path(
|
|
"config.json", tmp_path, required_extensions=(".json", ".yaml")
|
|
)
|
|
assert result is not None
|
|
assert result.suffix == ".json"
|
|
|
|
def test_extension_filtering_invalid(self, tmp_path):
|
|
with pytest.raises(ValueError, match="file type"):
|
|
PathValidator.validate_safe_path(
|
|
"script.py", tmp_path, required_extensions=(".json",)
|
|
)
|
|
|
|
def test_empty_input_rejected(self, tmp_path):
|
|
with pytest.raises(ValueError, match="Invalid path input"):
|
|
PathValidator.validate_safe_path("", tmp_path)
|
|
|
|
def test_none_input_rejected(self, tmp_path):
|
|
with pytest.raises(ValueError, match="Invalid path input"):
|
|
PathValidator.validate_safe_path(None, tmp_path)
|
|
|
|
def test_simple_filename(self, tmp_path):
|
|
result = PathValidator.validate_safe_path("readme.txt", tmp_path)
|
|
assert result.name == "readme.txt"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# validate_local_filesystem_path()
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestValidateLocalFilesystemPath:
|
|
"""Local filesystem path validation."""
|
|
|
|
def test_etc_restricted(self):
|
|
with pytest.raises(ValueError, match="system directories"):
|
|
PathValidator.validate_local_filesystem_path("/etc/passwd")
|
|
|
|
def test_sys_restricted(self):
|
|
with pytest.raises(ValueError, match="system directories"):
|
|
PathValidator.validate_local_filesystem_path("/sys/kernel")
|
|
|
|
def test_proc_restricted(self):
|
|
with pytest.raises(ValueError, match="system directories"):
|
|
PathValidator.validate_local_filesystem_path("/proc/self/environ")
|
|
|
|
def test_dev_restricted(self):
|
|
with pytest.raises(ValueError, match="system directories"):
|
|
PathValidator.validate_local_filesystem_path("/dev/null")
|
|
|
|
def test_tilde_expansion(self):
|
|
result = PathValidator.validate_local_filesystem_path(
|
|
"~/Documents", restricted_dirs=[]
|
|
)
|
|
assert str(result).startswith(str(Path.home()))
|
|
|
|
def test_tilde_alone(self):
|
|
result = PathValidator.validate_local_filesystem_path(
|
|
"~", restricted_dirs=[]
|
|
)
|
|
assert result == Path.home().resolve()
|
|
|
|
def test_control_chars_rejected(self):
|
|
with pytest.raises(ValueError, match="Control characters"):
|
|
PathValidator.validate_local_filesystem_path("/tmp/file\x01name")
|
|
|
|
def test_null_bytes_rejected(self):
|
|
with pytest.raises(ValueError, match="[Nn]ull"):
|
|
PathValidator.validate_local_filesystem_path("/tmp/file\x00name")
|
|
|
|
def test_dotdot_blocked(self):
|
|
with pytest.raises(ValueError, match="traversal"):
|
|
PathValidator.validate_local_filesystem_path("/tmp/../etc/passwd")
|
|
|
|
def test_absolute_unix_path_valid(self):
|
|
result = PathValidator.validate_local_filesystem_path("/tmp")
|
|
assert result.is_absolute()
|
|
|
|
def test_relative_path_resolved(self):
|
|
result = PathValidator.validate_local_filesystem_path("somedir")
|
|
assert result.is_absolute()
|
|
|
|
def test_empty_rejected(self):
|
|
with pytest.raises(ValueError, match="Invalid path input"):
|
|
PathValidator.validate_local_filesystem_path("")
|
|
|
|
def test_none_rejected(self):
|
|
with pytest.raises(ValueError, match="Invalid path input"):
|
|
PathValidator.validate_local_filesystem_path(None)
|
|
|
|
def test_custom_restricted_dirs(self):
|
|
with pytest.raises(ValueError, match="system directories"):
|
|
PathValidator.validate_local_filesystem_path(
|
|
"/custom/restricted/path",
|
|
restricted_dirs=[Path("/custom/restricted")],
|
|
)
|
|
|
|
def test_boot_restricted(self):
|
|
with pytest.raises(ValueError, match="system directories"):
|
|
PathValidator.validate_local_filesystem_path("/boot/vmlinuz")
|
|
|
|
def test_var_log_restricted(self):
|
|
with pytest.raises(ValueError, match="system directories"):
|
|
PathValidator.validate_local_filesystem_path("/var/log/syslog")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# sanitize_for_filesystem_ops()
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSanitizeForFilesystemOps:
|
|
"""Re-sanitization for static analyzers."""
|
|
|
|
def test_non_absolute_rejected(self):
|
|
with pytest.raises(ValueError, match="absolute"):
|
|
PathValidator.sanitize_for_filesystem_ops(Path("relative/path"))
|
|
|
|
def test_absolute_path_passes(self):
|
|
result = PathValidator.sanitize_for_filesystem_ops(Path("/tmp/safe"))
|
|
assert result == Path("/tmp/safe")
|
|
|
|
def test_root_path(self):
|
|
result = PathValidator.sanitize_for_filesystem_ops(Path("/"))
|
|
assert result == Path("/")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# validate_config_path()
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestValidateConfigPath:
|
|
"""Config file path validation."""
|
|
|
|
def test_etc_prefix_rejected(self):
|
|
with pytest.raises(ValueError, match="restricted"):
|
|
PathValidator.validate_config_path("/etc/config.json")
|
|
|
|
def test_proc_prefix_rejected(self):
|
|
with pytest.raises(ValueError, match="restricted"):
|
|
PathValidator.validate_config_path("/proc/config.yaml")
|
|
|
|
def test_sys_prefix_rejected(self):
|
|
with pytest.raises(ValueError, match="restricted"):
|
|
PathValidator.validate_config_path("/sys/config.toml")
|
|
|
|
def test_dev_prefix_rejected(self):
|
|
with pytest.raises(ValueError, match="restricted"):
|
|
PathValidator.validate_config_path("/dev/config.ini")
|
|
|
|
def test_dotdot_blocked(self):
|
|
with pytest.raises(ValueError, match="traversal"):
|
|
PathValidator.validate_config_path("../etc/passwd")
|
|
|
|
def test_empty_rejected(self):
|
|
with pytest.raises(ValueError, match="Invalid config path"):
|
|
PathValidator.validate_config_path("")
|
|
|
|
def test_none_rejected(self):
|
|
with pytest.raises(ValueError, match="Invalid config path"):
|
|
PathValidator.validate_config_path(None)
|
|
|
|
def test_null_bytes_rejected(self):
|
|
with pytest.raises(ValueError, match="[Nn]ull"):
|
|
PathValidator.validate_config_path("/tmp/config\x00.json")
|
|
|
|
def test_relative_path_uses_config_root(self, tmp_path):
|
|
result = PathValidator.validate_config_path(
|
|
"settings.json", config_root=str(tmp_path)
|
|
)
|
|
assert str(tmp_path) in str(result)
|
|
|
|
def test_invalid_extension_rejected(self, tmp_path):
|
|
# Create a file with invalid extension
|
|
test_file = tmp_path / "script.py"
|
|
test_file.write_text("data")
|
|
with pytest.raises(ValueError, match="file type"):
|
|
PathValidator.validate_config_path(
|
|
"script.py", config_root=str(tmp_path)
|
|
)
|
|
|
|
def test_relative_config_path_with_config_root(self, tmp_path):
|
|
cfg = tmp_path / "cfg.json"
|
|
cfg.write_text("{}")
|
|
# validate_config_path uses safe_join("/", path) which requires
|
|
# stripping the leading slash; pass relative-to-root form
|
|
result = PathValidator.validate_config_path(
|
|
"cfg.json", config_root=str(tmp_path)
|
|
)
|
|
assert result.name == "cfg.json"
|
|
|
|
def test_absolute_config_path_rejected_for_invalid_extension(
|
|
self, tmp_path
|
|
):
|
|
script = tmp_path / "config.py"
|
|
script.write_text("data")
|
|
with pytest.raises(ValueError, match="file type|Invalid absolute path"):
|
|
PathValidator.validate_config_path(str(script))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SAFE_PATH_PATTERN regex
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSafePathPattern:
|
|
"""SAFE_PATH_PATTERN regex validation."""
|
|
|
|
def test_alphanumeric(self):
|
|
assert PathValidator.SAFE_PATH_PATTERN.match("hello123")
|
|
|
|
def test_dots_and_dashes(self):
|
|
assert PathValidator.SAFE_PATH_PATTERN.match("file-name.txt")
|
|
|
|
def test_slashes(self):
|
|
assert PathValidator.SAFE_PATH_PATTERN.match("dir/sub/file.txt")
|
|
|
|
def test_spaces_rejected(self):
|
|
assert PathValidator.SAFE_PATH_PATTERN.match("hello world") is None
|
|
|
|
def test_special_chars_rejected(self):
|
|
assert PathValidator.SAFE_PATH_PATTERN.match("file;rm -rf") is None
|
|
|
|
def test_shell_injection_rejected(self):
|
|
assert PathValidator.SAFE_PATH_PATTERN.match("$(whoami)") is None
|
|
|
|
def test_underscore_allowed(self):
|
|
assert PathValidator.SAFE_PATH_PATTERN.match("my_file.txt")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CONFIG_EXTENSIONS
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestConfigExtensions:
|
|
def test_json_included(self):
|
|
assert ".json" in PathValidator.CONFIG_EXTENSIONS
|
|
|
|
def test_yaml_included(self):
|
|
assert ".yaml" in PathValidator.CONFIG_EXTENSIONS
|
|
assert ".yml" in PathValidator.CONFIG_EXTENSIONS
|
|
|
|
def test_toml_included(self):
|
|
assert ".toml" in PathValidator.CONFIG_EXTENSIONS
|