Skip to content

Commit 4f3e6a6

Browse files
committed
fix: builtins subscript bypass, /bin/rm normalize, filter args-as-list crash
- _python_scanner: Subscript(__builtins__/globals/vars/locals[...]) now detected as dynamic exec sink (was complete bypass) - _python_scanner: remove 3-arg getattr exemption entirely - _bash_scanner: normalize cmd with basename so /bin/rm matches rm check - _safety_filter: safe extraction when req['args'] is list not dict
1 parent 7120ab2 commit 4f3e6a6

3 files changed

Lines changed: 31 additions & 10 deletions

File tree

trpc_agent_sdk/tools/safety/_bash_scanner.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,8 @@ def _analyse_one_command(self, line_no: int, raw_line: str, cmd_tokens: List[str
314314
return # pure assignment/prefix, no real command
315315
cmd = cmd_tokens[idx]
316316
args = cmd_tokens[idx + 1:]
317-
cmd_lower = cmd.lower()
317+
# Normalise: /bin/rm, /usr/bin/rm → rm
318+
cmd_lower = cmd.lower().rsplit("/", 1)[-1]
318319
args_str = " ".join(args)
319320
evidence = " ".join(cmd_tokens)[:300]
320321

trpc_agent_sdk/tools/safety/_python_scanner.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -483,10 +483,6 @@ 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(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
490486
else:
491487
self._findings.append(
492488
PythonScanFinding(kind="eval_exec",
@@ -559,6 +555,15 @@ def _check_dynamic_call(self, node: ast.Call, line_no: int, evidence: str) -> No
559555
line_number=line_no,
560556
evidence=evidence,
561557
))
558+
elif isinstance(func, ast.Subscript):
559+
# __builtins__["exec"](code) / globals()["__builtins__"]["eval"](x)
560+
container = self._resolve_canonical(func.value)
561+
if container in ("__builtins__", "globals", "vars", "locals"):
562+
self._findings.append(
563+
PythonScanFinding(kind="eval_exec",
564+
canonical_name=f"{container}[...]",
565+
line_number=line_no,
566+
evidence=evidence))
562567
elif isinstance(func, ast.Attribute):
563568
# __import__("os").system("id") — the receiver is a dynamic
564569
# import call whose result was then attribute-accessed

trpc_agent_sdk/tools/safety/_safety_filter.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -252,9 +252,14 @@ def _extract_list_field(req: Any, *keys: str) -> Optional[list[str]]:
252252
"""Extract a list-typed field from the request by trying multiple key names."""
253253
if isinstance(req, dict):
254254
for k in keys:
255-
val = req.get(k) or req.get("args", {}).get(k)
255+
val = req.get(k)
256256
if isinstance(val, list):
257257
return val
258+
args = req.get("args")
259+
if isinstance(args, dict):
260+
val = args.get(k)
261+
if isinstance(val, list):
262+
return val
258263
if hasattr(req, "args"):
259264
args = getattr(req, "args")
260265
if isinstance(args, dict):
@@ -266,20 +271,30 @@ def _extract_list_field(req: Any, *keys: str) -> Optional[list[str]]:
266271

267272

268273
def _extract_str_field(req: Any, *keys: str) -> Optional[str]:
269-
"""Extract a string-typed field from the request by trying multiple key names."""
274+
"""Extract a string-typed field, safe against args-as-list."""
270275
if isinstance(req, dict):
271276
for k in keys:
272-
val = req.get(k) or req.get("args", {}).get(k)
277+
val = req.get(k)
273278
if isinstance(val, str):
274279
return val
280+
args = req.get("args")
281+
if isinstance(args, dict):
282+
val = args.get(k)
283+
if isinstance(val, str):
284+
return val
275285
return None
276286

277287

278288
def _extract_dict_field(req: Any, *keys: str) -> Optional[dict[str, str]]:
279-
"""Extract a dict-typed field from the request by trying multiple key names."""
289+
"""Extract a dict-typed field, safe against args-as-list."""
280290
if isinstance(req, dict):
281291
for k in keys:
282-
val = req.get(k) or req.get("args", {}).get(k)
292+
val = req.get(k)
283293
if isinstance(val, dict):
284294
return val
295+
args = req.get("args")
296+
if isinstance(args, dict):
297+
val = args.get(k)
298+
if isinstance(val, dict):
299+
return val
285300
return None

0 commit comments

Comments
 (0)