Dev.to Security 🔐 Cybersecurity 👁 0 📖 11 min read

The Allowlist Guarded the Destination, Not the Route: A Proxies Parameter Leaked Credentials From an Agent HTTP Tool

TL;DR CVE-2026-18394 (GitHub advisory GHSA-qhw6-2h72-m84v, AWS bulletin 2026-069-AWS, published July 31, 2026) is a credential leak in the http_request tool of strands-agents-tools. An operator could bind a token to a

TL;DR

  • CVE-2026-18394 (GitHub advisory GHSA-qhw6-2h72-m84v, AWS bulletin 2026-069-AWS, published July 31, 2026) is a credential leak in the http_request tool of strands-agents-tools. An operator could bind a token to approved hostnames with HTTP_REQUEST_TOKEN_CONFIG, but the tool also let the model choose a proxies parameter. A prompt injection could point that proxy at an attacker, and the credential would travel to the wrong hop.
  • The fix is one deleted schema property. The patch (PR #513, shipped in v0.8.2 on June 25, 2026) removed proxies from the model-visible input, so proxy settings now come only from operator-controlled HTTP_PROXY and HTTPS_PROXY. I diffed the two versions: 22 schema properties became 21, nothing added.
  • The allowlist checked urlparse(url).hostname and nothing else. It did not look at scheme or port, and it had no opinion about the route a request would take. I reproduced the consequence locally with a fake token: a proxy received the credential in cleartext for an http:// target, and saw only a CONNECT line for an https:// target.
  • In a small illustrative model, a hostname-only gate blocked 1 of 5 credential-leaking requests, an exact-origin gate blocked 4, and an exact-origin gate plus a fixed route blocked all 5, with legitimate requests unaffected.
  • Timing matters today: the package itself was deprecated on November 4, 2026 (v0.9.0 turns every tool into an import error), so pin audits and migrations are due now.

What did CVE-2026-18394 actually break?

Agent HTTP tools have a problem that ordinary HTTP clients do not: the caller is a language model that reads untrusted text. When an agent fetches a web page, a ticket or a README, whatever is in that content can try to steer the next tool call. That is indirect prompt injection, and it is the threat the advisory describes.

The Strands http_request tool let operators attach secrets to requests safely, in theory. You listed an environment variable and the hostnames it may be sent to, like this (the example comes from the tool's own docstring):

from strands_tools.http_request import HTTP_REQUEST_TOKEN_CONFIG
HTTP_REQUEST_TOKEN_CONFIG["GITHUB_TOKEN"] = ["api.github.com"]

The model then passed auth_env_var="GITHUB_TOKEN" and a URL. The tool refused any URL whose hostname was not in the list. That control is sound for what it covers: the model cannot ask for the GitHub token to be sent to evil.example.

The gap was a second input the model could also set. The PR description puts it plainly: the proxies parameter "is included in the LLM-controllable input schema, allowing an LLM influenced by indirect prompt injection to route credentialed requests through actor-controlled proxy infrastructure, bypassing the HTTP_REQUEST_TOKEN_CONFIG hostname allowlist." The AWS bulletin adds the consequence: "The credential is attached, and the request is routed through the actor's proxy on the first hop, disclosing the credential in cleartext."

The GitHub advisory rates it Moderate with CVSS 6.9 and classifies it as CWE-863, incorrect authorization. At least one vulnerability aggregator lists 7.4. Scorers disagree, so read the vector and your own exposure, not the number. The advisory also notes a second weakness in the same neighborhood: GET requests bypassed the consent gate. I confirmed that in the v0.8.1 source, where the confirmation dialog applies only to POST, PUT, PATCH and DELETE. A credentialed GET to a hostile route therefore needed no human in the loop.

Why did a correct allowlist still leak?

Because it guarded the destination, and a credential's journey has more than one position. Before the request reaches api.github.com, it passes through whatever the client was told to use as a proxy. That first hop sees the request, and the allowlist had no say over it.

This is the same shape as the lesson from the symlink and trailing-dot bugs I covered earlier: a check is only as strong as the set of inputs it actually constrains. Here the check constrained the URL's hostname, while the egress route, the scheme and the port were all outside it.

Here is the check in v0.8.1, trimmed to the part that matters:

allowed_domains = HTTP_REQUEST_TOKEN_CONFIG.get(env_var_name)
request_host = urlparse(tool_input.get("url", "")).hostname or ""
if request_host not in allowed_domains:
    raise ValueError(f"Request to '{request_host}' is not in the allowed domains ...")

.hostname drops the scheme and the port. So http://api.github.com/user and https://api.github.com:8443/user both pass a list containing api.github.com. Whether that matters depends on what the credential rides on, so I measured it rather than guessing.

What does an unintended proxy actually see?

I wrote a short script that starts throwaway listeners on 127.0.0.1 and uses the requests library (2.34.2) with a fake token. Nothing leaves the machine. The relevant part:

SECRET = "ghp_FAKE0000demo"
# case 1: plain-http target, proxy chosen by the "model"
requests.get("http://api.github.com/user",
             headers={"Authorization": f"token {SECRET}"},
             proxies={"http": f"http://127.0.0.1:{proxy_port}"})
# case 2: https target through the same kind of proxy
requests.get("https://api.github.com/user",
             headers={"Authorization": f"token {SECRET}"},
             proxies={"https": f"http://127.0.0.1:{proxy_port2}"})

The listener for case 1 received this, secret included:

GET http://api.github.com/user HTTP/1.1
Host: api.github.com
Authorization: token ghp_FAKE0000demo

The listener for case 2 received only CONNECT api.github.com:443 HTTP/1.1 and Host: api.github.com:443. The secret was not in it, because with certificate verification on, the headers travel inside the TLS tunnel.

That second result refines the headline, it does not contradict it. The bulletin's "cleartext" statement holds when the approved hostname is reached over plain HTTP, and a hostname-only check does not stop that. The tool also gates disabling certificate verification behind a separate environment variable (STRANDS_HTTP_ALLOW_INSECURE_SSL), which is why the TLS case stayed closed in my run. I did not test every deployment shape, and I did not run the attack against a real Strands agent. Treat the experiment as a demonstration of the mechanism, and the advisory as the authority on the product.

I also checked one more egress knob that remained after the patch: redirects. The tool exposes allow_redirects, and requests follows them by default. Against a second local listener on a different port, the Authorization header was stripped on the cross-origin hop, but a custom X-API-Key header was forwarded. The tool supports auth_type="api_key", which sets exactly that header. This is general requests behavior, not a reported Strands flaw, and exploiting it would need an open redirect on an approved host. I include it because it shows how many places "where does this credential go" can be decided.

Measured locally: did the unintended hop see the secret?

How big was the fix?

Small, which is the instructive part. PR #513 changed two files, with 14 additions and 26 deletions. It deleted the proxies property from the tool's JSON schema, deleted the doc example that showed a proxy, and stopped forwarding tool_input.get("proxies") to requests. The test that asserted "the proxy was passed through" now asserts the opposite: an LLM-supplied proxies field must not reach requests.

I extracted the TOOL_SPEC from both tagged versions and diffed the property names:

v0.8.1 properties: 22
v0.8.2 properties: 21
removed: ['proxies'] | added: []
route-affecting keys still LLM-visible in 0.8.2: ['allow_redirects', 'auth_env_var', 'url', 'verify_ssl']

The last line is my own classification of which remaining properties influence where a request goes or how it is protected. I am not claiming any of them is exploitable. The point is that after a fix like this, the right question is the one the patch author implicitly asked: which knobs on this tool should a model ever hold? The patch's own comment answers it for proxies: "Proxies are an operator/deployment concern, not something the model should choose."

Which gate design would have stopped it?

Here is a runnable model of three gate designs against five credential-leaking requests and two legitimate ones. It is illustrative, not a benchmark of any product, and gate C is my design, not what Strands shipped (they removed the knob).

from urllib.parse import urlparse
APPROVED_HOSTS = {"api.github.com"}
APPROVED_ORIGINS = {("https", "api.github.com", 443)}

def origin(url):
    u = urlparse(url)
    return (u.scheme, u.hostname, u.port or {"http": 80, "https": 443}.get(u.scheme))

def gate_a(req): return (urlparse(req["url"]).hostname or "") in APPROVED_HOSTS   # hostname only
def gate_b(req): return origin(req["url"]) in APPROVED_ORIGINS                     # scheme + host + port
def gate_c(req): return gate_b(req) and not req.get("proxies") \
                        and not req.get("follow_redirects_to_other_origin")        # origin + fixed route

Running it:

attack                                             A    B    C
http:// URL + model-chosen proxy                   LEAK blk  blk
http:// URL, no proxy (cleartext on the wire)      LEAK blk  blk
same host, other port                              LEAK blk  blk
approved origin, redirect to another origin        LEAK LEAK blk
unapproved host                                    blk  blk  blk
blocked of 5 {'A': 1, 'B': 4, 'C': 5}
legit allowed: {'A': 2, 'B': 2, 'C': 2} of 2

Credential-leaking requests blocked, out of 5

Three observations. Gate A, the hostname check, stops only the case everybody thinks of. Gate B closes three more by comparing the whole origin, and it costs nothing on legitimate traffic. Gate C closes the last one, the route, and it does so by removing the model's say over routing rather than by inspecting the route.

That last design principle is the one to carry away. You can try to validate a model-supplied proxy URL, but the validation has to know every host you consider safe, every scheme, every redirect, and every environment variable the HTTP library reads. Taking the parameter out of the schema is a smaller attack surface than any validator, and it is the option the maintainers chose.

A cheap regression guard is a CI test that fails when someone adds a property to a credentialed tool:

REVIEWED = {"method", "url", "headers", "body", "timeout", "auth_type", "auth_env_var"}

def test_credentialed_tool_schema_is_reviewed(tool_spec):
    props = set(tool_spec["inputSchema"]["json"]["properties"])
    assert props <= REVIEWED, f"unreviewed model-visible parameters: {sorted(props - REVIEWED)}"

When the test fails, a person has to decide whether the model should hold the new knob. That one review step would have caught proxies the day it was added.

Why does the timeline matter right now?

Because the vulnerable tool's home is closing. From the release notes:

Date Event
2026-06-25 PR #513 merged, v0.8.2 released with proxies removed (the fix)
2026-07-31 AWS bulletin 2026-069-AWS published (36 days after the fix)
2026-09-15 v0.8.9 lists http_request among deprecated tools (82 days after the fix)
2026-11-04 v0.9.0: package and repository deprecated, no updates for earlier versions, deprecation warnings become import errors (132 days after the fix)

Across v0.8.9 to v0.8.12 (September 15 to November 2), the release notes list 34 tools by name as deprecated, including python_repl, mcp_client, browser, code_interpreter, use_computer, file_write, editor and use_aws. The notes do not give a reason for the deprecations, and I will not guess one. The v0.9.0 notes say the same capabilities now ship as built-in tools in the Strands Harness SDK, with a migration guide linked from the release.

strands-agents-tools: tools deprecated per release (34 total)

Practical consequences for anyone running it:

  1. Check your pin. Anything below 0.8.2 has the bug. The bulletin also tells you to rotate credentials configured through HTTP_REQUEST_TOKEN_CONFIG on affected versions "even if there is no indication a credential was exposed."
  2. Do not just upgrade to the latest. v0.9.0 replaces deprecation warnings with import errors for all tools. It will not give you a patched http_request; it will break the import.
  3. Re-run your own review on the replacement. I did not audit the SDK's built-in tools. The questions in this post (which parameters can the model set, which hops can a credential visit, which methods skip consent) apply to them as much as to the old package.

What is a short checklist for credentialed agent tools?

  • List every parameter the model can set, then mark the ones that decide where bytes go: URL, scheme, port, proxy, redirect policy, TLS verification, base URL, DNS override.
  • Bind credentials to an origin (scheme, host, port), not a hostname.
  • Put routing in operator configuration, outside the model-visible schema.
  • Do not follow redirects with credentials attached unless the target origin is also approved.
  • Apply consent to credentialed reads too. A GET that carries a secret is not a read-only action.
  • Add a schema-allowlist test so a new parameter forces a review.
  • Rotate after the fact. A leaked token that was never noticed is still leaked.

FAQ

What is CVE-2026-18394?
An incorrect-authorization flaw (CWE-863) in the http_request tool of strands-agents-tools before 0.8.2. The model could set a proxies parameter, so an indirect prompt injection could route a credentialed request through an attacker-controlled proxy and bypass the hostname allowlist for HTTP_REQUEST_TOKEN_CONFIG.

Which versions are affected and what is the fix?
Versions before 0.8.2 are affected. Version 0.8.2 removes proxies from the model-visible input. Proxies are now configured only through HTTP_PROXY and HTTPS_PROXY in the environment, or in code. The advisory also recommends rotating credentials configured on affected versions.

Would HTTPS have protected the token?
In my local test, a proxy on an https:// request saw only the CONNECT line and not the Authorization header, because certificate verification was on. The bulletin's cleartext scenario applies when the approved hostname is requested over plain HTTP, which a hostname-only allowlist permits. Do not rely on TLS to rescue a gate that ignores the scheme.

Is an allowlist enough for credentialed tools?
Only if it covers the origin and the route. A hostname list alone does not constrain scheme, port, proxy or redirects. Pin the exact origin, keep routing out of the model's hands, and add consent for credentialed requests regardless of HTTP method.

Should I upgrade to v0.9.0?
Not as a security fix. The release deprecates the package and turns every tool into an import error. Stay on a patched version only as a bridge, and plan the migration to the replacement tools, reviewing their parameters before you give them credentials.

Further reading

Earlier posts in this series:

  • The "Read-Only" Fast Path Is Your Weakest Gate: A Newline Let Hidden Commands Through an Agent's Bash Check (published 2026-11-05, same series)
  • The Model Is Not a Security Boundary: What Two Semantic Kernel CVEs Teach About Gating Tool Arguments (published 2026-10-22, same series)

Sources

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.