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

477 lines
19 KiB
Python

"""
Module Whitelist Tests
Tests for the dynamic import security module that prevents arbitrary code execution
through user-controlled configuration values.
Security model:
- Only allows relative imports (starting with ".")
- Validates module paths against a strict whitelist of trusted search engine modules
- Validates class names against a strict whitelist of legitimate search engine classes
- Blocks dangerous modules like os, subprocess, sys, etc.
- Prevents path traversal attacks in module paths
"""
import pytest
from unittest.mock import MagicMock, patch
from local_deep_research.security.module_whitelist import (
ALLOWED_CLASS_NAMES,
ALLOWED_MODULE_PATHS,
ModuleNotAllowedError,
SecurityError,
get_safe_module_class,
validate_module_import,
)
class TestAllowedModulePaths:
"""Test the ALLOWED_MODULE_PATHS constant."""
def test_constants_are_relative_paths(self):
"""All allowed module paths should start with '.' (relative imports)."""
for module_path in ALLOWED_MODULE_PATHS:
assert module_path.startswith("."), (
f"Module path '{module_path}' should be a relative import "
f"(starting with '.'). Absolute imports are not allowed for security."
)
def test_whitelist_is_frozen_set(self):
"""ALLOWED_MODULE_PATHS should be a frozenset (immutable)."""
assert isinstance(ALLOWED_MODULE_PATHS, frozenset)
def test_whitelist_contains_expected_engines(self):
"""Whitelist should contain expected search engine modules."""
expected_engines = [
".engines.search_engine_brave",
".engines.search_engine_ddg",
".engines.search_engine_tavily",
".engines.search_engine_wikipedia",
]
for engine in expected_engines:
assert engine in ALLOWED_MODULE_PATHS, (
f"Expected engine '{engine}' not found in whitelist"
)
class TestAllowedClassNames:
"""Test the ALLOWED_CLASS_NAMES constant."""
def test_class_names_whitelist_is_frozen_set(self):
"""ALLOWED_CLASS_NAMES should be a frozenset (immutable)."""
assert isinstance(ALLOWED_CLASS_NAMES, frozenset)
def test_whitelist_contains_expected_classes(self):
"""Whitelist should contain expected search engine classes."""
expected_classes = [
"BraveSearchEngine",
"DuckDuckGoSearchEngine",
"TavilySearchEngine",
"WikipediaSearchEngine",
"BaseSearchEngine",
]
for cls_name in expected_classes:
assert cls_name in ALLOWED_CLASS_NAMES, (
f"Expected class '{cls_name}' not found in whitelist"
)
class TestLegacyAliases:
"""Test backward compatibility aliases."""
def test_module_not_allowed_error_is_security_error(self):
"""ModuleNotAllowedError should be an alias for SecurityError."""
assert ModuleNotAllowedError is SecurityError
class TestValidateModuleImport:
"""Test the validate_module_import function."""
def test_valid_module_and_class(self):
"""Should return True for valid whitelisted module and class."""
result = validate_module_import(
".engines.search_engine_brave", "BraveSearchEngine"
)
assert result is True
def test_valid_base_search_engine(self):
"""Should return True for the base search engine module."""
result = validate_module_import(
".search_engine_base", "BaseSearchEngine"
)
assert result is True
def test_rejects_absolute_path(self):
"""Should reject absolute module paths."""
# Absolute path attempt
result = validate_module_import(
"local_deep_research.web_search_engines.engines.search_engine_brave",
"BraveSearchEngine",
)
assert result is False
def test_rejects_subprocess_module(self):
"""Should reject dangerous subprocess module."""
result = validate_module_import("subprocess", "Popen")
assert result is False
def test_rejects_os_module(self):
"""Should reject dangerous os module."""
result = validate_module_import("os", "system")
assert result is False
def test_rejects_sys_module(self):
"""Should reject dangerous sys module."""
result = validate_module_import("sys", "exit")
assert result is False
def test_case_sensitivity(self):
"""Module and class names should be case-sensitive."""
# Lowercase - should fail
result = validate_module_import(
".engines.search_engine_brave", "bravesearchengine"
)
assert result is False
# Uppercase - should fail
result = validate_module_import(
".ENGINES.SEARCH_ENGINE_BRAVE", "BraveSearchEngine"
)
assert result is False
def test_empty_module_path(self):
"""Should reject empty module path."""
result = validate_module_import("", "BraveSearchEngine")
assert result is False
def test_empty_class_name(self):
"""Should reject empty class name."""
result = validate_module_import(".engines.search_engine_brave", "")
assert result is False
def test_none_module_path(self):
"""Should reject None module path."""
result = validate_module_import(None, "BraveSearchEngine")
assert result is False
def test_none_class_name(self):
"""Should reject None class name."""
result = validate_module_import(".engines.search_engine_brave", None)
assert result is False
def test_valid_module_invalid_class(self):
"""Should reject valid module with invalid class name."""
result = validate_module_import(
".engines.search_engine_brave", "MaliciousClass"
)
assert result is False
def test_invalid_module_valid_class(self):
"""Should reject invalid module with valid class name."""
result = validate_module_import(
".engines.nonexistent_engine", "BraveSearchEngine"
)
assert result is False
class TestGetSafeModuleClass:
"""Test the get_safe_module_class function."""
def test_loads_valid_search_engine(self):
"""Should successfully load a valid whitelisted search engine class."""
# Use a module that exists and should be loadable
cls = get_safe_module_class(".search_engine_base", "BaseSearchEngine")
assert cls is not None
assert cls.__name__ == "BaseSearchEngine"
def test_raises_security_error_for_invalid_module(self):
"""Should raise SecurityError for non-whitelisted module."""
with pytest.raises(SecurityError) as exc_info:
get_safe_module_class("os", "system")
assert "not in the security whitelist" in str(exc_info.value)
def test_rejects_absolute_import_from_own_package(self):
"""Should reject absolute paths — normalization was removed."""
with pytest.raises(SecurityError):
get_safe_module_class(
"local_deep_research.web_search_engines.search_engine_base",
"BaseSearchEngine",
)
def test_raises_security_error_for_foreign_absolute_import(self):
"""Should raise SecurityError for absolute import paths outside our package."""
with pytest.raises(SecurityError):
get_safe_module_class(
"local_deep_research.security.module_whitelist",
"SecurityError",
)
def test_handles_module_not_found(self):
"""Should raise ModuleNotFoundError if whitelisted module doesn't exist."""
# First, temporarily add a fake module to the whitelist for testing
# We can't actually test this without modifying the whitelist, so we mock
with patch(
"local_deep_research.security.module_whitelist.validate_module_import",
return_value=True,
):
with patch(
"local_deep_research.security.module_whitelist.importlib.import_module",
side_effect=ModuleNotFoundError("No module named 'fake'"),
):
with pytest.raises(ModuleNotFoundError):
get_safe_module_class(".engines.fake_module", "FakeEngine")
def test_handles_attribute_not_found(self):
"""Should raise AttributeError if class doesn't exist in module."""
with patch(
"local_deep_research.security.module_whitelist.validate_module_import",
return_value=True,
):
mock_module = MagicMock(spec=[]) # Module without the class
with patch(
"local_deep_research.security.module_whitelist.importlib.import_module",
return_value=mock_module,
):
with pytest.raises(AttributeError):
get_safe_module_class(
".engines.search_engine_brave",
"NonExistentClass",
)
def test_uses_default_package_for_relative_imports(self):
"""Should use default package when none provided for relative imports."""
with patch(
"local_deep_research.security.module_whitelist.validate_module_import",
return_value=True,
):
with patch(
"local_deep_research.security.module_whitelist.importlib.import_module"
) as mock_import:
mock_module = MagicMock()
mock_module.FakeEngine = type("FakeEngine", (), {})
mock_import.return_value = mock_module
get_safe_module_class(".engines.fake", "FakeEngine")
# Check that default package was used
mock_import.assert_called_once_with(
".engines.fake",
package="local_deep_research.web_search_engines",
)
def test_uses_custom_package_when_provided(self):
"""Should use custom package when explicitly provided."""
with patch(
"local_deep_research.security.module_whitelist.validate_module_import",
return_value=True,
):
with patch(
"local_deep_research.security.module_whitelist.importlib.import_module"
) as mock_import:
mock_module = MagicMock()
mock_module.FakeEngine = type("FakeEngine", (), {})
mock_import.return_value = mock_module
get_safe_module_class(
".engines.fake",
"FakeEngine",
package="custom.package",
)
mock_import.assert_called_once_with(
".engines.fake",
package="custom.package",
)
def test_rejects_collection_engine_absolute_path(self):
"""Absolute paths are rejected — normalization was removed."""
with pytest.raises(SecurityError):
get_safe_module_class(
"local_deep_research.web_search_engines.engines.search_engine_collection",
"CollectionSearchEngine",
)
def test_rejects_exact_prefix_as_absolute(self):
"""Exact prefix without module suffix is rejected as absolute path."""
with pytest.raises(SecurityError):
get_safe_module_class(
"local_deep_research.web_search_engines",
"BaseSearchEngine",
)
def test_prefix_boundary_rejects_similar_prefix(self):
"""Prefix that extends the package name without a dot should NOT be normalized.
'local_deep_research.web_search_enginesXXX.evil' should not match
the normalization prefix 'local_deep_research.web_search_engines.'.
"""
with pytest.raises(SecurityError):
get_safe_module_class(
"local_deep_research.web_search_enginesXXX.evil",
"BaseSearchEngine",
)
def test_rejects_absolute_path_from_own_package_without_normalization(self):
"""Absolute paths are rejected — registry injection means normalization is unnecessary."""
with pytest.raises(SecurityError):
get_safe_module_class(
"local_deep_research.web_search_engines.engines.search_engine_searxng",
"SearXNGSearchEngine",
)
class TestSecurityScenarios:
"""Test specific security attack scenarios."""
def test_blocks_os_system_import(self):
"""Should block attempts to import os.system for command execution."""
with pytest.raises(SecurityError):
get_safe_module_class("os", "system")
def test_blocks_subprocess_import(self):
"""Should block attempts to import subprocess for command execution."""
with pytest.raises(SecurityError):
get_safe_module_class("subprocess", "Popen")
with pytest.raises(SecurityError):
get_safe_module_class("subprocess", "call")
with pytest.raises(SecurityError):
get_safe_module_class("subprocess", "run")
def test_path_traversal_in_module_path(self):
"""Should block path traversal attempts in module paths."""
# Attempt to break out of expected package
malicious_paths = [
"..os",
"....subprocess",
".engines/../../../os",
".engines/..\\..\\subprocess", # Windows-style
".engines%2F..%2F..%2Fos", # URL-encoded
]
for path in malicious_paths:
result = validate_module_import(path, "system")
assert result is False, (
f"Path traversal attempt '{path}' should be blocked"
)
def test_blocks_builtins_module(self):
"""Should block attempts to import builtins."""
with pytest.raises(SecurityError):
get_safe_module_class("builtins", "eval")
with pytest.raises(SecurityError):
get_safe_module_class("builtins", "__import__")
def test_blocks_importlib_module(self):
"""Should block attempts to import importlib directly."""
with pytest.raises(SecurityError):
get_safe_module_class("importlib", "import_module")
def test_blocks_code_module(self):
"""Should block attempts to import code execution modules."""
with pytest.raises(SecurityError):
get_safe_module_class("code", "interact")
def test_blocks_pickle_module(self):
"""Should block attempts to import pickle (deserialization attacks)."""
with pytest.raises(SecurityError):
get_safe_module_class("pickle", "loads")
def test_blocks_ctypes_module(self):
"""Should block attempts to import ctypes (memory manipulation)."""
with pytest.raises(SecurityError):
get_safe_module_class("ctypes", "CDLL")
def test_blocks_socket_module(self):
"""Should block attempts to import socket (network access)."""
with pytest.raises(SecurityError):
get_safe_module_class("socket", "socket")
def test_sql_injection_style_attack(self):
"""Should block SQL-injection-style attacks in module names."""
malicious_inputs = [
(".engines; import os;", "system"),
('.engines"; import os; "', "system"),
(".engines' OR '1'='1", "BraveSearchEngine"),
]
for module_path, class_name in malicious_inputs:
result = validate_module_import(module_path, class_name)
assert result is False, (
f"Injection attempt '{module_path}' should be blocked"
)
def test_unicode_bypass_attempt(self):
"""Should handle unicode normalization bypass attempts."""
# These are unlikely to work but good to test
unicode_attempts = [
"\u006fs", # 'o' followed by 's' -> 'os'
"o\u0073", # 'o' followed by 's' -> 'os'
]
for module_path in unicode_attempts:
# Even if they normalize to 'os', should still be blocked
result = validate_module_import(module_path, "system")
assert result is False
class TestSecurityErrorException:
"""Test the SecurityError exception class."""
def test_security_error_is_exception(self):
"""SecurityError should be an Exception."""
assert issubclass(SecurityError, Exception)
def test_security_error_message(self):
"""SecurityError should contain helpful error message."""
error = SecurityError("Test error message")
assert "Test error message" in str(error)
def test_security_error_can_be_raised_and_caught(self):
"""SecurityError should be raisable and catchable."""
with pytest.raises(SecurityError) as exc_info:
raise SecurityError("blocked import")
assert "blocked import" in str(exc_info.value)
class TestDocumentation:
"""Documentation tests explaining the security model."""
@pytest.mark.skip(reason="documentation/placeholder test - not implemented")
def test_security_model_documentation(self):
"""
Document the module whitelist security model.
WHY THIS EXISTS:
- Dynamic imports from user configuration can lead to arbitrary code execution
- Attackers could inject malicious module paths to run arbitrary code
- Example: config.module = "os"; config.class = "system" -> command execution
THE WHITELIST APPROACH:
- Only explicitly approved modules can be loaded
- Only explicitly approved class names can be instantiated
- All imports must be relative (starting with '.') for additional safety
- Relative imports are anchored to 'local_deep_research.web_search_engines'
DEFENSE IN DEPTH:
- Whitelist validation before any import attempt
- Relative import requirement prevents direct access to dangerous modules
- Class name whitelist prevents instantiation of dangerous classes
- Logging of blocked attempts for security auditing
SECURITY MODEL:
| Input Type | Action | Reason |
|-------------------------|------------|-------------------------------------|
| Relative + listed | ALLOWED | Trusted search engine module |
| Absolute (any) | BLOCKED | Could access dangerous modules |
| Unlisted module | BLOCKED | Not in trusted whitelist |
| Unlisted class | BLOCKED | Could be dangerous class |
| Empty/None | BLOCKED | Invalid input |
"""
# audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: DELETE).
assert True # Documentation test