mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-07-15 12:58:04 +00:00
fix(security): match the sensitive-file deny-list case-insensitively (#5097)
This commit is contained in:
+18
-8
@@ -71,25 +71,35 @@ _SENSITIVE_FILE_PATTERNS: tuple[str, ...] = (
|
|||||||
"known_hosts",
|
"known_hosts",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Case-folded views used for matching. On a case-insensitive filesystem
|
||||||
|
# (Windows, default macOS) ".SSH/AUTHORIZED_KEYS" and ".env" resolve to the
|
||||||
|
# same protected files as their lowercase forms, so the deny-list has to fold
|
||||||
|
# case before comparing — the sibling resolver already normcases paths for the
|
||||||
|
# same reason. casefold (not os.path.normcase) because normcase is a no-op on
|
||||||
|
# POSIX, which is exactly where the macOS read-exfil path lives.
|
||||||
|
_SENSITIVE_BASENAMES_CF: frozenset[str] = frozenset(b.casefold() for b in _SENSITIVE_BASENAMES)
|
||||||
|
_SENSITIVE_FILE_PATTERNS_CF: frozenset[str] = frozenset(p.casefold() for p in _SENSITIVE_FILE_PATTERNS)
|
||||||
|
|
||||||
|
|
||||||
def _is_sensitive_path(resolved: str) -> bool:
|
def _is_sensitive_path(resolved: str) -> bool:
|
||||||
"""Return True if *resolved* falls under a sensitive directory or
|
"""Return True if *resolved* falls under a sensitive directory or
|
||||||
matches a sensitive filename — regardless of what root it sits under.
|
matches a sensitive filename — regardless of what root it sits under.
|
||||||
|
|
||||||
|
Matching is case-insensitive: on Windows / default macOS a case-variant
|
||||||
|
name (``.SSH``, ``AUTHORIZED_KEYS``, ``Id_Rsa``) points at the same file as
|
||||||
|
the lowercase form, so a case-sensitive check would let it slip past the
|
||||||
|
deny-list in every file tool that relies on it.
|
||||||
"""
|
"""
|
||||||
parts = resolved.split(os.sep)
|
parts = [p.casefold() for p in resolved.split(os.sep)]
|
||||||
filenames: set[str] = {parts[-1]} if parts else set()
|
filename = parts[-1] if parts else ""
|
||||||
|
|
||||||
# Check if any path component is a sensitive directory.
|
# Check if any path component is a sensitive directory.
|
||||||
for part in parts:
|
for part in parts:
|
||||||
if part in _SENSITIVE_BASENAMES:
|
if part in _SENSITIVE_BASENAMES_CF:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Check filename against known sensitive files.
|
# Check filename against known sensitive files.
|
||||||
for pat in _SENSITIVE_FILE_PATTERNS:
|
return filename in _SENSITIVE_FILE_PATTERNS_CF
|
||||||
if pat in filenames:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _tool_path_roots() -> list[str]:
|
def _tool_path_roots() -> list[str]:
|
||||||
|
|||||||
@@ -57,6 +57,27 @@ def test_non_sensitive_path():
|
|||||||
assert not _is_sensitive_path("/home/user/projects/file.py")
|
assert not _is_sensitive_path("/home/user/projects/file.py")
|
||||||
|
|
||||||
|
|
||||||
|
def test_sensitive_case_insensitive():
|
||||||
|
"""On case-insensitive filesystems (Windows, default macOS) a case-variant
|
||||||
|
name resolves to the same protected file, so the deny-list must match
|
||||||
|
regardless of case. Built with os.path.join so the separator is right on
|
||||||
|
both POSIX and Windows.
|
||||||
|
"""
|
||||||
|
from src.tool_execution import _is_sensitive_path
|
||||||
|
# sensitive directory, varied case
|
||||||
|
assert _is_sensitive_path(os.path.join("home", "u", ".SSH", "authorized_keys"))
|
||||||
|
assert _is_sensitive_path(os.path.join("home", "u", ".Gnupg", "pubring.kbx"))
|
||||||
|
# sensitive filename, varied case
|
||||||
|
assert _is_sensitive_path(os.path.join("ws", "AUTHORIZED_KEYS"))
|
||||||
|
assert _is_sensitive_path(os.path.join("ws", "Id_Rsa"))
|
||||||
|
assert _is_sensitive_path(os.path.join("ws", ".ENV"))
|
||||||
|
assert _is_sensitive_path(os.path.join("ws", ".Env"))
|
||||||
|
# both dir and file varied
|
||||||
|
assert _is_sensitive_path(os.path.join("home", "u", ".SSH", "AUTHORIZED_KEYS"))
|
||||||
|
# an ordinary file with none of the sensitive names is still allowed
|
||||||
|
assert not _is_sensitive_path(os.path.join("ws", "Readme.md"))
|
||||||
|
|
||||||
|
|
||||||
# ── Unit tests on _resolve_tool_path ─────────────────────────────────
|
# ── Unit tests on _resolve_tool_path ─────────────────────────────────
|
||||||
|
|
||||||
def test_blocks_etc_shadow():
|
def test_blocks_etc_shadow():
|
||||||
|
|||||||
Reference in New Issue
Block a user