Files
wehub-resource-sync 7a0da7932b
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
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

153 lines
5.4 KiB
Python

"""Tests for path traversal protection in PDF storage manager (PR #1890).
Verifies:
- _get_safe_file_path blocks traversal, symlinks, and sentinel values
- library_root is resolved on init
- get_absolute_path_from_settings validates resolved paths
"""
import pytest
from unittest.mock import MagicMock, patch
from local_deep_research.constants import (
FILE_PATH_METADATA_ONLY,
FILE_PATH_TEXT_ONLY,
FILE_PATH_BLOB_DELETED,
)
from local_deep_research.research_library.services.pdf_storage_manager import (
PDFStorageManager,
)
class TestValidateFilesystemPath:
"""Test _get_safe_file_path blocks unsafe paths."""
def test_traversal_blocked(self, tmp_path):
"""Path with .. escaping library root returns None."""
manager = PDFStorageManager(
library_root=str(tmp_path), storage_mode="filesystem"
)
result = manager._get_safe_file_path("../../etc/passwd")
assert result is None
def test_valid_relative_path(self, tmp_path):
"""Valid relative path inside library root returns resolved Path."""
sub = tmp_path / "docs"
sub.mkdir(exist_ok=True)
test_file = sub / "paper.pdf"
test_file.write_text("pdf content")
manager = PDFStorageManager(
library_root=str(tmp_path), storage_mode="filesystem"
)
result = manager._get_safe_file_path("docs/paper.pdf")
assert result is not None
assert result.is_relative_to(tmp_path)
def test_metadata_only_returns_none(self, tmp_path):
manager = PDFStorageManager(
library_root=str(tmp_path), storage_mode="filesystem"
)
result = manager._get_safe_file_path(FILE_PATH_METADATA_ONLY)
assert result is None
def test_text_only_returns_none(self, tmp_path):
manager = PDFStorageManager(
library_root=str(tmp_path), storage_mode="filesystem"
)
result = manager._get_safe_file_path(FILE_PATH_TEXT_ONLY)
assert result is None
def test_blob_deleted_returns_none(self, tmp_path):
"""blob_deleted sentinel should return None (not construct a path)."""
manager = PDFStorageManager(
library_root=str(tmp_path), storage_mode="filesystem"
)
result = manager._get_safe_file_path(FILE_PATH_BLOB_DELETED)
assert result is None
def test_empty_string_returns_none(self, tmp_path):
manager = PDFStorageManager(
library_root=str(tmp_path), storage_mode="filesystem"
)
result = manager._get_safe_file_path("")
assert result is None
def test_none_returns_none(self, tmp_path):
manager = PDFStorageManager(
library_root=str(tmp_path), storage_mode="filesystem"
)
result = manager._get_safe_file_path(None)
assert result is None
def test_symlink_blocked(self, tmp_path):
"""Symlinks are blocked before resolve() dereferences them."""
manager = PDFStorageManager(
library_root=str(tmp_path), storage_mode="filesystem"
)
# Create a symlink pointing outside library root
link_path = tmp_path / "evil_link"
try:
link_path.symlink_to("/etc/passwd")
except OSError:
pytest.skip("Cannot create symlinks in this environment")
result = manager._get_safe_file_path("evil_link")
assert result is None
class TestLibraryRootResolved:
"""Verify library_root is resolved on init (prevents late traversal)."""
def test_library_root_is_resolved(self, tmp_path):
"""library_root should be an absolute resolved path."""
manager = PDFStorageManager(
library_root=str(tmp_path), storage_mode="filesystem"
)
assert manager.library_root == tmp_path.resolve()
assert manager.library_root.is_absolute()
class TestGetAbsolutePathFromSettings:
"""Test path traversal protection in get_absolute_path_from_settings."""
@patch("local_deep_research.utilities.db_utils.get_settings_manager")
def test_traversal_returns_none(self, mock_settings_mgr, tmp_path):
from local_deep_research.research_library.utils import (
get_absolute_path_from_settings,
)
mock_mgr = MagicMock()
mock_mgr.get_setting.return_value = str(tmp_path)
mock_settings_mgr.return_value = mock_mgr
result = get_absolute_path_from_settings("../../etc/passwd")
assert result is None
@patch("local_deep_research.utilities.db_utils.get_settings_manager")
def test_valid_path_returns_resolved(self, mock_settings_mgr, tmp_path):
from local_deep_research.research_library.utils import (
get_absolute_path_from_settings,
)
mock_mgr = MagicMock()
mock_mgr.get_setting.return_value = str(tmp_path)
mock_settings_mgr.return_value = mock_mgr
result = get_absolute_path_from_settings("docs/file.pdf")
assert result.is_relative_to(tmp_path.resolve())
@patch("local_deep_research.utilities.db_utils.get_settings_manager")
def test_empty_path_returns_library_root(self, mock_settings_mgr, tmp_path):
from local_deep_research.research_library.utils import (
get_absolute_path_from_settings,
)
mock_mgr = MagicMock()
mock_mgr.get_setting.return_value = str(tmp_path)
mock_settings_mgr.return_value = mock_mgr
result = get_absolute_path_from_settings("")
assert result == tmp_path.resolve()