Harden module installation and imports
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
from collections.abc import Callable, Iterable, Mapping, Sequence
|
||||
from dataclasses import dataclass, field
|
||||
from importlib import import_module
|
||||
@@ -15,6 +16,10 @@ from govoplan_core.server.fastapi import LifespanFactory
|
||||
ManifestFactory = Callable[[], ModuleManifest]
|
||||
RouterEnabled = Callable[[object | None, PlatformRegistry], bool]
|
||||
AppConfigurator = Callable[[FastAPI, PlatformRegistry, object | None], None]
|
||||
DEFAULT_TRUSTED_IMPORT_PREFIXES = ("govoplan_core", "govoplan_", "app")
|
||||
TRUSTED_IMPORT_PREFIXES_ENV = "GOVOPLAN_TRUSTED_IMPORT_PREFIXES"
|
||||
_MODULE_PATH_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$")
|
||||
_ATTRIBUTE_PATH_RE = re.compile(r"^[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z][A-Za-z0-9_]*)*$")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -49,16 +54,52 @@ class GovoplanServerConfig:
|
||||
|
||||
|
||||
def import_object(path: str) -> Any:
|
||||
module_name, separator, attribute = path.partition(":")
|
||||
if not separator:
|
||||
raise ValueError(f"Object path must use module:attribute syntax: {path!r}")
|
||||
module = import_module(module_name)
|
||||
module_name, attribute = validate_object_path(path)
|
||||
_validate_trusted_import_prefix(module_name, trusted_import_prefixes())
|
||||
module = import_module(module_name) # nosemgrep: python.lang.security.audit.non-literal-import.non-literal-import
|
||||
value: Any = module
|
||||
for part in attribute.split("."):
|
||||
value = getattr(value, part)
|
||||
return value
|
||||
|
||||
|
||||
def validate_object_path(path: str) -> tuple[str, str]:
|
||||
module_name, separator, attribute = path.strip().partition(":")
|
||||
if not separator:
|
||||
raise ValueError(f"Object path must use module:attribute syntax: {path!r}")
|
||||
if not _MODULE_PATH_RE.fullmatch(module_name):
|
||||
raise ValueError(f"Object path module is not a valid Python module path: {path!r}")
|
||||
if not _ATTRIBUTE_PATH_RE.fullmatch(attribute) or any(part.startswith("_") for part in attribute.split(".")):
|
||||
raise ValueError(f"Object path attribute is not a public attribute path: {path!r}")
|
||||
return module_name, attribute
|
||||
|
||||
|
||||
def trusted_import_prefixes() -> tuple[str, ...]:
|
||||
configured = os.getenv(TRUSTED_IMPORT_PREFIXES_ENV, "").strip()
|
||||
if not configured:
|
||||
return DEFAULT_TRUSTED_IMPORT_PREFIXES
|
||||
prefixes = tuple(item.strip() for item in configured.split(",") if item.strip())
|
||||
return prefixes or DEFAULT_TRUSTED_IMPORT_PREFIXES
|
||||
|
||||
|
||||
def _validate_trusted_import_prefix(module_name: str, prefixes: Iterable[str]) -> None:
|
||||
if any(_module_matches_prefix(module_name, prefix) for prefix in prefixes):
|
||||
return
|
||||
raise ValueError(
|
||||
f"Object path module {module_name!r} is outside trusted import prefixes. "
|
||||
f"Set {TRUSTED_IMPORT_PREFIXES_ENV} to allow custom deployment config modules."
|
||||
)
|
||||
|
||||
|
||||
def _module_matches_prefix(module_name: str, prefix: str) -> bool:
|
||||
cleaned = prefix.strip()
|
||||
if not cleaned:
|
||||
return False
|
||||
if cleaned.endswith((".", "_")):
|
||||
return module_name.startswith(cleaned)
|
||||
return module_name == cleaned or module_name.startswith(f"{cleaned}.")
|
||||
|
||||
|
||||
def load_server_config(path: str | None = None) -> GovoplanServerConfig:
|
||||
config_path = path or os.getenv("GOVOPLAN_SERVER_CONFIG") or "govoplan_core.server.default_config:get_server_config"
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user