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
153 lines
5.4 KiB
Python
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()
|