fix(security): consolidation & securite phase 1 (#84, BUG-021 a BUG-034)
- sanitizer XSS serveur (markdown + page de partage) [BUG-021/022] - rate-limit/lockout MFA [BUG-023] - isolation vaults par segments [BUG-024] - caps regex ReDoS [BUG-025] - SSRF webhooks + secrets externalises [BUG-026] - rotation/revocation des jetons [BUG-027] - politique de mot de passe + invalidation sessions [BUG-028] - verrous users.json [BUG-029] - IP reelle dans les audits [BUG-030] - rate-limit par compte [BUG-031] - symlinks hors vault ignores [BUG-032] - recherche simple via inverted index [BUG-033] - token en memoire + cookie HttpOnly, CSP durcie [BUG-034] Tests: pytest 961 passed / 6 skipped, ruff 0, mypy 0, frontend vert.
This commit is contained in:
+29
-12
@@ -15,6 +15,27 @@ from backend.services.errors import ServiceError
|
||||
logger = logging.getLogger("obsigate.services.paths")
|
||||
|
||||
|
||||
def _is_within(resolved: Path, root: Path) -> bool:
|
||||
"""Return True when *resolved* is *root* or lives below it.
|
||||
|
||||
The comparison is segment-aware so that a sibling directory whose name
|
||||
merely shares a prefix (``vault`` vs ``vault-evil``) is rejected. A
|
||||
case-insensitive fallback preserves the Windows / Docker behaviour where
|
||||
the resolved casing can differ from the configured root.
|
||||
"""
|
||||
try:
|
||||
resolved.relative_to(root)
|
||||
return True
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
resolved_parts = tuple(part.lower() for part in resolved.parts)
|
||||
root_parts = tuple(part.lower() for part in root.parts)
|
||||
except Exception:
|
||||
return False
|
||||
return resolved_parts[: len(root_parts)] == root_parts
|
||||
|
||||
|
||||
def resolve_safe_path(vault_root: Path, relative_path: str | None) -> Path:
|
||||
"""Resolve a vault-relative path, rejecting traversal outside the vault.
|
||||
|
||||
@@ -30,16 +51,12 @@ def resolve_safe_path(vault_root: Path, relative_path: str | None) -> Path:
|
||||
logger.error(f"Path resolution error - vault_root: {vault_root}, relative_path: {relative_path}, error: {e}")
|
||||
raise ServiceError(f"Path resolution error: {e!s}", code="path_error", status=500) from e
|
||||
|
||||
try:
|
||||
resolved.relative_to(root)
|
||||
except ValueError:
|
||||
# Case-insensitive fallback for Windows / Docker path casing.
|
||||
if not str(resolved).lower().startswith(str(root).lower()):
|
||||
logger.warning(f"Path outside vault - vault: {root}, requested: {relative_path}, resolved: {resolved}")
|
||||
raise ServiceError(
|
||||
"Access denied: path outside vault",
|
||||
code="path_outside_vault",
|
||||
status=403,
|
||||
details={"path": relative_path},
|
||||
) from None
|
||||
if not _is_within(resolved, root):
|
||||
logger.warning(f"Path outside vault - vault: {root}, requested: {relative_path}, resolved: {resolved}")
|
||||
raise ServiceError(
|
||||
"Access denied: path outside vault",
|
||||
code="path_outside_vault",
|
||||
status=403,
|
||||
details={"path": relative_path},
|
||||
)
|
||||
return resolved
|
||||
|
||||
Reference in New Issue
Block a user