Skip to content

Commit 7120ab2

Browse files
committed
fix: 3-arg getattr bypass + policy race + re.compile W504
- _python_scanner: 3-arg getattr only exempted when NOT immediately called. getattr(os,system,None)('id') now correctly flagged as dyn-exec. x=getattr(cfg,key,def) as plain expression still safe. - _policy: get_policy/reload_policy now use threading.Lock to prevent concurrent double-load and inconsistent scanner cache. - _python_scanner: remove W504 line break after binary operator.
1 parent fa456cd commit 7120ab2

2 files changed

Lines changed: 16 additions & 9 deletions

File tree

trpc_agent_sdk/tools/safety/_policy.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
import hashlib
1515
import os
16+
import threading
1617
from dataclasses import dataclass
1718
from dataclasses import field
1819
from pathlib import Path
@@ -228,18 +229,22 @@ def _compute_hash(self) -> str:
228229
# ---------------------------------------------------------------------------
229230

230231
_default_policy: Optional[SafetyPolicy] = None
232+
_policy_lock = threading.Lock()
231233

232234

233235
def get_policy(policy_path: Optional[str] = None) -> SafetyPolicy:
234-
"""Return the cached policy or load it from disk on first call."""
236+
"""Return the cached policy or load it from disk on first call (thread-safe)."""
235237
global _default_policy # pylint: disable=global-statement
236238
if _default_policy is None:
237-
_default_policy = PolicyLoader(policy_path).load()
239+
with _policy_lock:
240+
if _default_policy is None:
241+
_default_policy = PolicyLoader(policy_path).load()
238242
return _default_policy
239243

240244

241245
def reload_policy(policy_path: Optional[str] = None) -> SafetyPolicy:
242-
"""Force-reload the policy from disk."""
246+
"""Force-reload the policy from disk (thread-safe)."""
243247
global _default_policy # pylint: disable=global-statement
244-
_default_policy = PolicyLoader(policy_path).load()
248+
with _policy_lock:
249+
_default_policy = PolicyLoader(policy_path).load()
245250
return _default_policy

trpc_agent_sdk/tools/safety/_python_scanner.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -483,10 +483,10 @@ def _handle_call(self, node: ast.Call) -> None:
483483
receiver = self._resolve_canonical(node.func.value) if isinstance(node.func, ast.Attribute) else ""
484484
if canonical == "compile" and receiver in ("re", "regex"):
485485
pass # re.compile(pattern) — safe, pattern compilation only
486-
# getattr with 3+ args (including default) is safe property access
487-
elif canonical == "getattr" and (len(node.args) >= 3 or
488-
(isinstance(node.func, ast.Call) and len(node.func.args) >= 3)):
489-
pass # getattr(obj, attr, default) / getattr(...)() — safe
486+
# getattr(x, y, default) as plain expression (NOT immediately
487+
# called) is safe property access.
488+
elif (canonical == "getattr" and len(node.args) >= 3 and not isinstance(node.func, ast.Call)):
489+
pass
490490
else:
491491
self._findings.append(
492492
PythonScanFinding(kind="eval_exec",
@@ -537,7 +537,9 @@ def _check_dynamic_call(self, node: ast.Call, line_no: int, evidence: str) -> No
537537
if isinstance(func, ast.Call):
538538
inner = self._resolve_canonical(func.func)
539539
if inner == "getattr":
540-
# getattr(obj, attr, default) with 3 args is safe property access
540+
# getattr(obj, attr, default) with 3 args and NOT immediately
541+
# called: safe property access (e.g. x = getattr(cfg, "key", 42)).
542+
# Called form getattr(os,"system")(...) is caught by _handle_call.
541543
if len(func.args) >= 3:
542544
pass
543545
else:

0 commit comments

Comments
 (0)