studio/sandbox: use _find_sensitive_paths for binding-bias instead of substring
Refines the round-5 ``_looks_sensitive`` heuristic that biases
``_record_string_binding`` toward the dangerous value in a chained
reassignment. The substring hint set conflated ``/etc/shadow`` with
``/etc/hosts`` -- both contain ``/etc/`` -- so a payload like
``p = '/etc/hosts'; p = '/etc/shadow'; open(p)`` had ``cur`` already
flagged sensitive, the guard refused to update, and the resolved
value stayed at ``/etc/hosts`` (allow-listed). The chained shadow
binding then slipped through.
Now ``_looks_sensitive`` delegates to ``_find_sensitive_paths``, the
authoritative bash / file gate matcher, so the distinction is exact:
``/etc/hosts`` is allow-listed and ``/etc/shadow`` is sensitive.
``_record_string_binding`` also adopts a clean three-way rule mirroring
Python's last-wins semantics for sensitive values:
* New sensitive value: always wins (covers the chained shadow case).
* New benign value, current sensitive: keep current (static gate
cannot prove the new value executes; err on blocking).
* Both benign: latest seen wins.
Test suite still 528 passing.
This commit is contained in:
parent
b038848482
commit
35d7b72832
1 changed files with 26 additions and 35 deletions
|
|
@ -1364,46 +1364,33 @@ def _check_signal_escape_patterns(code: str):
|
|||
if alias.name in _PATHLIB_PATH_CLASSES_PREPASS:
|
||||
path_class_aliases_prepass.add(alias.asname or alias.name)
|
||||
|
||||
# Cheap hint set used to bias ``string_bindings`` toward the most
|
||||
# sensitive value of a name when multiple literals are assigned.
|
||||
# Substring match against the full set of credential / process-state
|
||||
# root tokens (kept intentionally loose; the real per-spec match
|
||||
# still runs downstream in the file-read / shutil / bash gates).
|
||||
_SENSITIVE_HINTS = (
|
||||
"/etc/",
|
||||
"/proc/",
|
||||
"/var/spool/",
|
||||
"/root/",
|
||||
".ssh/",
|
||||
".aws/",
|
||||
".gnupg",
|
||||
".kube",
|
||||
".docker",
|
||||
".config/gcloud",
|
||||
".pypirc",
|
||||
".npmrc",
|
||||
".netrc",
|
||||
".cargo/credentials",
|
||||
".password-store",
|
||||
"credentials",
|
||||
"id_rsa",
|
||||
"id_dsa",
|
||||
"id_ecdsa",
|
||||
"id_ed25519",
|
||||
"shadow",
|
||||
"sudoers",
|
||||
)
|
||||
|
||||
def _looks_sensitive(value: str) -> bool:
|
||||
"""True if *value* matches any host-credential / process-state
|
||||
path that the bash / file gates already flag. Uses the
|
||||
authoritative ``_find_sensitive_paths`` so the bias distinguishes
|
||||
``/etc/shadow`` (sensitive) from ``/etc/hosts`` (allow-listed) --
|
||||
a substring hint set conflates them and admits a chained-
|
||||
reassignment bypass ``p='/etc/hosts'; p='/etc/shadow'``."""
|
||||
if not value:
|
||||
return False
|
||||
low = value.lower()
|
||||
return any(hint in low for hint in _SENSITIVE_HINTS)
|
||||
return bool(_find_sensitive_paths(value))
|
||||
|
||||
def _record_string_binding(name: str, value: str) -> None:
|
||||
"""Append ``value`` to ``string_bindings_all[name]`` and update
|
||||
``string_bindings[name]`` to favour a sensitive-shaped value
|
||||
when one exists. Resists the second-assignment bypass."""
|
||||
``string_bindings[name]`` so the gate sees the most sensitive
|
||||
value the variable could carry at runtime. The selection rule
|
||||
mirrors Python's last-wins semantics for sensitive values:
|
||||
|
||||
* If the new value is sensitive, it always wins (even if the
|
||||
current is also sensitive) -- a later sensitive assignment
|
||||
is at least as concerning as an earlier one, and the chain
|
||||
``p='/etc/hosts'; p='/etc/shadow'`` must surface the shadow.
|
||||
* If the new value is benign and the current sensitive, keep
|
||||
the sensitive value (Python would last-wins to benign, but
|
||||
statically we cannot prove the new value executes and we
|
||||
err on the side of blocking the path the attacker reached
|
||||
for).
|
||||
* If both are benign, latest seen wins."""
|
||||
bucket = string_bindings_all.setdefault(name, [])
|
||||
if value not in bucket:
|
||||
bucket.append(value)
|
||||
|
|
@ -1411,8 +1398,12 @@ def _check_signal_escape_patterns(code: str):
|
|||
if cur is None:
|
||||
string_bindings[name] = value
|
||||
return
|
||||
if _looks_sensitive(value) and not _looks_sensitive(cur):
|
||||
if _looks_sensitive(value):
|
||||
string_bindings[name] = value
|
||||
return
|
||||
if _looks_sensitive(cur):
|
||||
return
|
||||
string_bindings[name] = value
|
||||
|
||||
def _extract_string_literal(node, _depth = 0):
|
||||
"""Strict literal-string extraction: no name binding lookup,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue