# Why query-string credentials leak before anyone reads them

A secret in a URL has already taken the scenic route. It may have crossed a client library, a proxy, an access log, a tracing system, a browser history database, and an exported CSV before the API receives it. HTTPS protects that request on the wire. It does not make every machine and service that handles the URL forget it.

That is why an autonomous agent can avoid seeing a token and still cause a credential leak. If the agent asks an action gateway to call `https://api.example.test/v1/builds?access_token=...`, the gateway can keep the token out of the agent transcript yet still construct a URL that other systems habitually record. The secret moved out of the model's reach, but it landed in a much wider set of places.

The fix is less glamorous than secret scanning. Keep the URL limited to resource identity and ordinary filters. Put credentials in the request header, or in a request body when the protocol calls for it. Then make the component that holds the secret inject it at the last possible moment. This separates a request that is safe to name, replay in a test, and include in an audit record from a request that carries authorization.

## A URL is a record, not just a route

A URL is designed to be copied, displayed, compared, cached, bookmarked, and logged. Those properties make it useful for resource identity and terrible for bearer material. A query parameter becomes part of the request target, which many layers treat as routine operational data.

CWE-598 calls this weakness "Use of HTTP Request With Sensitive Query String". Its background notes name the usual escape routes: browser history, Referer headers, web logs, and other recording sources. The mitigation is equally plain: send sensitive information in headers or the request body instead. That advice is not a rule that GET is forbidden. It is a warning that putting a secret in the URI changes the number of people and systems that can recover it.

RFC 9110 makes the same distinction in its security considerations. It warns that URI query fields built from user input can carry sensitive data and says that a different server-generated URI can remove sensitive data from later links. I would go one step further for API credentials: do not create a secret-bearing URI in the first place. Replacing it later leaves copies behind.

A URL parameter is sometimes called "just an API key" or "only a short-lived token". Neither label changes its exposure surface. A short-lived token may still be valid when a log shipper forwards it. An API key may authorize a narrow endpoint, but that endpoint may be enough to read data, create cost, or mint a better credential. Treat authorization data as secret until the service that issued it says otherwise.

## Access logs preserve the part people forget

Most HTTP access logs include the method, request target, status, size, and timing because operators need those fields to diagnose traffic. The request target includes the path and query string. A typical line looks like this:

```
203.0.113.24 - - [14/Jun/2026:12:42:18 +0000] "GET /v1/builds?access_token=sk_live_example HTTP/1.1" 200 481
```

The damaging part is not an exotic debug setting. It is the ordinary field an operator searches when a route starts returning errors. Redacting an `Authorization` header is common because teams expect it to contain secrets. Redacting arbitrary query keys is harder: one upstream calls it `token`, another uses `api_key`, a third accepts `sig`, and a fourth puts the credential inside a signed blob.

Do not accept "we redact logs" as an answer until someone can show the exact request target after redaction, including query values, at every hop. A rule that masks `token` misses `access_token`. A rule that masks `access_token` misses a vendor that calls the same value `key`. A rule that masks known names does nothing for a pre-signed URL where the credential is distributed across several parameters.

The practical distinction is this: header redaction is a defense around a secret-bearing request, while header injection prevents the URL from becoming secret-bearing. You still need log controls for headers. You also get to stop writing an endless list of query-name exceptions that always trails vendor APIs.

## Reverse proxies make one request into several records

A reverse proxy sees the full request before it forwards it. So do load balancers, API gateways, service meshes, WAFs, CDN edge services, and observability agents that instrument the request lifecycle. They do not all log the same format, retain data for the same time, or send it to the same account.

That matters because a clean application log proves very little. An ingress log may have the original URL. A proxy error log may repeat it when upstream connection setup fails. A trace span may attach `http.target` or a route value. A support bundle may package several of those files because someone needed help with a timeout. Each copy is operationally reasonable in isolation. Together they turn one leaked key into an inventory problem.

Teams often try to solve this with a global redaction filter. Use filters, but know their limit. A filter works only after a component has received the URL, parsed it correctly, and matched every spelling of the secret. It also creates pressure to keep URL credentials because removing them would require changing client code. The long-term fix belongs at the call boundary, where the credential joins the request.

Suppose an agent builds a deployment request. It can produce this safe action description:

```
GET https://deploy.example.test/v2/releases?project=docs-site&limit=20
credential: deploy-read
```

A credential-owning gateway resolves `deploy-read` inside its vault and sends `Authorization: Bearer ...` upstream. The proxy still sees a request, of course. Its request target contains `project` and `limit`, not the bearer value. If its header log accidentally exposes `Authorization`, that is a separate defect with a narrow, obvious test. Do not hide that defect, but do not multiply it with URL leakage.

## Browser history is a local leak with a long half-life

Browser history is not the main path for a headless agent, but it exposes a mistake in human workflows. Engineers paste a failing URL into a browser to inspect an error page, reproduce a callback, or prove an API route works. The browser stores the full address, suggests it later, and may sync history depending on local settings. A screen recording, a shared session, or a colleague borrowing the machine can surface it again.

The Referer header adds another route. When a browser loads a page whose address contains a query secret and that page requests a resource or follows a link, a downstream server may receive a referrer value subject to browser policy. Modern referrer policies reduce some cases, but they do not turn a secret URL into a sound design. The credential should not be present for policy to protect.

This is why "the agent never saw it" is not enough. A developer may see the URL in an agent result, paste it into a ticket, or use it in a manual check. Any system that displays a URL encourages copying it. Put an opaque secret name in the human-facing action, not a secret value.

There is one exception worth naming: a signed URL intentionally grants access through the URL itself. That may be the protocol a storage service offers for a temporary download. Handle it as a constrained capability, not as a normal API credential. Keep its expiry short, scope it to one object and method when the provider allows it, avoid printing it, and do not let an agent choose its own arbitrary query parameters. A signed URL is still sensitive in history and logs. Its temporary nature limits damage; it does not remove the leak path.

## Audit exports turn an incident into a distribution event

An audit trail should help you answer who requested an action, which credential identity authorized it, what destination received it, and what happened. It should not become a second credential store. The dangerous audit schema records a complete URL because that feels faithful. Faithful to what? It faithfully preserves a value that the investigator should never need.

Use an audit record that separates stable identifiers from secret material. A useful HTTP action record can include the method, scheme, host, path, non-sensitive query names and values, credential alias, session identity, decision, status, response classification, and timestamps. It can store a digest of selected request components if you need tamper evidence. It should exclude authorization values, cookie contents, and secret query values.

This shape also makes exports safer. JSON, CSV, and support archives leave their original access boundary. Someone sends them to a vendor, attaches them to a bug, saves them in a shared drive, or loads them into a spreadsheet. That is the normal life of an export. Design it so an export answers an investigation without becoming a rotation queue.

Sallyport projects agent sessions and individual calls from one encrypted, hash-chained audit log. Its `sp audit verify` command verifies that chain offline over ciphertext without a key. That proves the log has not been altered. It does not excuse recording secret URLs. Integrity and confidentiality solve different problems, and teams regularly blur them because both get called "audit security".

A tamper-evident log that contains a live credential can prove precisely when the credential leaked. A redacted log with no integrity story can be safe to share but hard to trust. You need both properties, applied to different fields.

## Header injection keeps the secret out of the action description

Bearer and custom-header injection work because the caller can describe the destination without possessing the credential. The gateway owns the mapping between a credential label and its encrypted vault entry. It constructs the request, adds the header, sends it, and returns the result. The agent receives neither the header value nor a placeholder it can expand.

For a bearer API, the shape is conceptually simple:

```
agent request
  method: GET
  url: https://metrics.example.test/v1/usage?team=infra
  credential: metrics-production

gateway outbound request
  GET /v1/usage?team=infra HTTP/1.1
  Host: metrics.example.test
  Authorization: Bearer [vault value]
```

The bracketed text is explanatory notation, not a value that should appear in a real transcript. In a well-designed boundary, the agent cannot ask to reveal it, save it in a file, or move it into a query string. The gateway treats the credential as data it can use, not data it can hand back.

Custom headers deserve the same treatment. Some services use `X-API-Key`, `Api-Key`, or a vendor-specific header instead of `Authorization`. The exact name changes, but the rule does not: the client-visible request description should refer to a credential identity, and the gateway should inject the value at execution time. Basic authentication also belongs in the header, although you should prefer a provider's stronger supported method when one exists.

Sallyport supports bearer, basic, and custom-header credential injection for HTTP calls. That is useful here because it lets an MCP-capable agent request an HTTP action without keeping the API key in its own context. The same boundary is not a magical sanitizer: the URL and any fields the agent supplies can still contain secrets if you let them. Validate the request shape and reject secret-like values in places that should remain public.

## POST does not repair a credential in the URL

Changing `GET` to `POST` while leaving `?api_key=...` in the URL changes almost nothing about this leak. Proxies still receive the request target. Access logs still often record it. Browsers and tools can still display it. CWE-598 explicitly notes that a query string can appear with methods other than GET.

Moving a credential into a request body may reduce accidental logging in some stacks, because access logs usually do not include bodies by default. That is not the same as header injection. Bodies are often captured by debugging middleware, API clients, error reporters, and request recording tools. They also make a credential part of the action payload an agent may be tempted to construct or echo.

Use the method and body required by the API. Put a secret in a body only when the protocol explicitly requires it, such as a token exchange that specifies form parameters. Then limit body logging, exclude the endpoint from broad request capture, and keep the exchange inside the component that owns the credential. Do not convert every read request into POST as a superstition. Preserve HTTP semantics and remove the secret from the URI.

A related bad recommendation is "URL-encode it and the logs are harmless". Percent encoding only changes representation. Anyone with the URL can decode it, and many log viewers already do. Base64 has the same problem. Encoding can make a query harder to scan with the naked eye while making redaction rules easier to miss.

## A safe migration starts with evidence, not a mass edit

Do not replace every query parameter. Query parameters such as `page`, `sort`, `project`, and `fields` are often legitimate, useful, and non-secret. Start by finding where credentials actually enter URLs, then change those call sites with a test that observes the outbound request.

Use this sequence:

1. Search source code, agent prompts, saved curl commands, test fixtures, dashboards, and runbooks for names such as `token`, `key`, `secret`, `signature`, and `credential`. Search for full URLs containing `?`, too. Names vary.
2. Collect representative access logs and trace data from every ingress layer. Confirm whether each request target records query values, not merely parameter names. Treat retained exports and support bundles as another layer.
3. Ask the API owner which header or body mechanism it supports. If it only accepts a query credential, document the exception, scope the credential tightly, and isolate that call from general-purpose agents.
4. Change the action interface from `url with secret` to `url plus credential alias`. Add a test that rejects a URL containing a known test token and asserts the outbound header contains it.
5. Rotate every credential that appeared in a URL, then remove old logs and exports under your retention process. Rotation without cleanup leaves historical exposure; cleanup without rotation leaves a still-live key in circulation.

The test is where many migrations fail. A unit test that checks only the final HTTP response cannot tell you whether the key went in the header or query string. Put a local test server behind the client and capture method, path, query, and headers separately. Assert that the query does not contain the test value and that only the intended header does.

For a gateway, add a refusal case. Feed it `https://api.example.test/v1/jobs?access_token=test-canary` with any credential alias and make it fail before the network call. This will catch a future prompt template or convenience wrapper that tries to put secrets back into URLs. A canary token should be unique and unusable outside the test.

## Redaction is still necessary after the design is fixed

Header injection shrinks the set of likely leaks. It does not make logs safe by decree. An application can echo an authorization header in an exception, a proxy can log all headers during a debug incident, or an agent can paste response data that contains a secret into a ticket. Keep redaction, access control, retention limits, and incident response.

But place those controls in the right order. First, keep credentials out of URLs and agent-visible request descriptions. Next, redact known sensitive headers and bodies in every logger that can capture them. Then restrict who can retrieve raw records and how long they live. Finally, rehearse rotation and export cleanup so the team can act when a control fails.

This ordering avoids a popular trap: treating a redaction pattern as permission to pass secrets everywhere. Redaction rules are fragile because they depend on names, formats, and parsers. A credential boundary is stronger because it controls who ever receives the value.

## Agent authorization should include the outbound destination

An agent that cannot read a token can still spend its authority. If it can choose any URL, it may send a valid credential to an unintended host through a confused configuration, a typo, an SSRF path, or a prompt that exploits a permissive action interface. Secret non-disclosure and destination control are separate requirements.

Bind a credential alias to the intended service and keep the host, scheme, and allowed path shape explicit. A gateway should reject a credential selected for `metrics.example.test` when the action names `metrics.example.test.evil.invalid`. It should also reject user-info tricks, unexpected redirects that forward credentials, and URLs that hide a host change behind encoding. These checks belong where the header is injected, because that is the last point with both the credential identity and parsed destination.

Sallyport's per-session authorization card identifies the new agent process by its code-signing authority, and per-call keys can require a click or Touch ID for each use. Those controls answer whether this process may invoke an action. They do not make an arbitrary destination safe, so keep the action configuration narrow enough that an approval means something concrete.

## The useful artifact is a request record you can share

A practical test of this design is simple: can you paste the action record into an incident ticket without starting a credential rotation? If the answer is no, the record contains too much.

Aim for a record like this:

```
request_id: 01J...
agent_session: signed-process-42
method: GET
destination: https://metrics.example.test/v1/usage
query: team=infra
credential_alias: metrics-production
authorization: injected, value omitted
result: 200, 481 bytes
```

This record gives an investigator enough to correlate the call, reproduce the route with a safe test credential, and ask why the agent selected `metrics-production`. It deliberately cannot authenticate anyone. If an investigator needs the secret itself, that is a separate privileged recovery process, not a field in routine telemetry.

The first action is usually a boring search through URLs in source, prompts, and exported logs. Do it anyway. The credential you find there may have been copied by systems you forgot existed, and the only clean answer is to stop creating that kind of URL.
