Skip to content

Commit f88f898

Browse files
committed
fix: double-escape regex bypass and multi-field scan
- Fix \d+ → \d+ in _SENSITIVE_PATHS_RE and _CRED_PATH_RE so /proc/<pid>/environ is correctly detected. - Fix \s → \s in _path_boundary_pattern for whitespace matching. - _extract_script_content now collects ALL script-bearing fields (code + command + ...) instead of returning the first hit.
1 parent 4f07d1d commit f88f898

4 files changed

Lines changed: 36 additions & 26 deletions

File tree

trpc_agent_sdk/tools/safety/_bash_scanner.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -745,7 +745,7 @@ def _strip_inline_comment(line: str) -> str:
745745
r"(?:/etc/(?:shadow|passwd|sudoers|hosts)|"
746746
r"~?/\.ssh|~?/\.gnupg|~?/\.aws|~?/\.gcloud|~?/\.azure|"
747747
r"\.env|\.pem|id_rsa|id_ed25519|id_ecdsa|"
748-
r"/proc/(?:self|\\d+)/(?:mem|cmdline|environ)|"
748+
r"/proc/(?:self|\d+)/(?:mem|cmdline|environ)|"
749749
r"/var/run/docker\.sock)",
750750
re.IGNORECASE,
751751
)

trpc_agent_sdk/tools/safety/_python_scanner.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -913,7 +913,7 @@ def get_python_concurrency(findings: List[PythonScanFinding]) -> List[PythonScan
913913
r"(?:\.ssh|\.gnupg|\.aws|\.gcloud|\.azure|\.pem|\.key|id_rsa|id_ed25519|id_ecdsa|"
914914
r"credentials|secrets|\.env|config\.json|"
915915
r"/etc/(?:shadow|passwd|sudoers|hosts)|"
916-
r"/proc/(?:self|\\d+)/(?:mem|cmdline|environ)|"
916+
r"/proc/(?:self|\d+)/(?:mem|cmdline|environ)|"
917917
r"/var/run/docker\.sock)",
918918
re.IGNORECASE,
919919
)

trpc_agent_sdk/tools/safety/_rules.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,7 @@ def _strip_python_comment_line(line: str) -> str:
145145
continue
146146

147147
# Triple-quote detection (simplified — checks for ''' or \"\"\")
148-
if (not in_single and not in_double
149-
and i + 2 < n and ch in ("'", '"')
150-
and line[i:i + 3] == ch * 3):
148+
if (not in_single and not in_double and i + 2 < n and ch in ("'", '"') and line[i:i + 3] == ch * 3):
151149
marker = ch * 3
152150
result.append(marker)
153151
i += 3
@@ -537,7 +535,8 @@ def __call__(
537535

538536
# FIXED: Check whitelist_commands — was dead code before
539537
# Whitelisted commands get downgraded to INFO or skipped
540-
if policy.is_command_whitelisted(cmd_key) and cmd_key not in ("|", "$(", "`", "&>", "nohup", "disown"):
538+
if policy.is_command_whitelisted(cmd_key) and cmd_key not in ("|", "$(", "`", "&>", "nohup",
539+
"disown"):
541540
# Explicitly whitelisted → informational only
542541
findings.append(
543542
_build_finding(
@@ -860,9 +859,9 @@ def _is_in_echo_string(line: str, pattern: str) -> bool:
860859
"""
861860
stripped = line.strip()
862861
# Only applies to echo / printf commands
863-
if not (stripped.startswith("echo ") or stripped.startswith("echo\t")
864-
or stripped.startswith("printf ") or stripped.startswith("printf\t")
865-
or stripped.startswith("/bin/echo ") or stripped.startswith("/usr/bin/echo ")):
862+
if not (stripped.startswith("echo ") or stripped.startswith("echo\t") or stripped.startswith("printf ")
863+
or stripped.startswith("printf\t") or stripped.startswith("/bin/echo ")
864+
or stripped.startswith("/usr/bin/echo ")):
866865
return False
867866
# Check if the pattern *matches* inside single or double quotes
868867
try:
@@ -900,7 +899,7 @@ def _path_boundary_pattern(path: str) -> str:
900899
escaped = re.escape(path)
901900
# If the path starts with a dot (like .env), require a path boundary before it
902901
if path.startswith("."):
903-
return r"(?:^|[\\s/'\"`;|&(])" + escaped + r"(?:$|[\\s/'\"`;|&)])"
902+
return r"(?:^|[\s/'\"`;|&(])" + escaped + r"(?:$|[\s/'\"`;|&)])"
904903
else:
905904
return escaped.replace(r"\*", ".*")
906905

trpc_agent_sdk/tools/safety/_safety_filter.py

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -160,33 +160,44 @@ def __init__(self, report):
160160

161161

162162
def _extract_script_content(req: Any) -> Optional[str]:
163-
"""Heuristically extract script-like content from a tool request."""
163+
"""Heuristically extract script-like content from a tool request.
164+
165+
All recognised script-bearing fields are collected and joined so that
166+
a request carrying both ``code`` (benign) and ``command`` (dangerous)
167+
does not bypass detection by hiding behind the first hit.
168+
"""
164169
if isinstance(req, str):
165170
return req
171+
parts: list[str] = []
172+
seen: set[str] = set()
173+
174+
def _collect(val: Any) -> None:
175+
if isinstance(val, str) and val.strip():
176+
stripped = val.strip()
177+
if stripped not in seen:
178+
parts.append(stripped)
179+
seen.add(stripped)
180+
166181
if isinstance(req, dict):
167-
# Common field names used to pass code / commands
168182
for key in ("code", "script", "command", "cmd", "shell", "source", "content", "text", "input"):
169-
val = req.get(key)
170-
if isinstance(val, str) and val.strip():
171-
return val
172-
# Check for MCP tool arguments
183+
_collect(req.get(key))
173184
args = req.get("args", {})
174185
if isinstance(args, dict):
175186
for key in ("code", "script", "command", "cmd", "shell"):
176-
val = args.get(key)
177-
if isinstance(val, str) and val.strip():
178-
return val
179-
# Check for keyword arguments
187+
_collect(args.get(key))
180188
kwargs = req.get("kwargs")
181189
if isinstance(kwargs, dict) and kwargs:
182-
return _extract_script_content(kwargs)
183-
# Try to get 'args' attribute from an object
190+
sub = _extract_script_content(kwargs)
191+
if sub:
192+
_collect(sub)
184193
if hasattr(req, "args") and isinstance(getattr(req, "args"), dict):
185-
return _extract_script_content(getattr(req, "args"))
194+
sub = _extract_script_content(getattr(req, "args"))
195+
if sub:
196+
_collect(sub)
186197
if hasattr(req, "script_content"):
187-
val = getattr(req, "script_content")
188-
if isinstance(val, str):
189-
return val
198+
_collect(getattr(req, "script_content"))
199+
200+
return "\n".join(parts) if parts else None
190201
return None
191202

192203

0 commit comments

Comments
 (0)