How credential injection should follow request validation
Credential injection request validation prevents API keys from reaching the wrong host, redirect target, header path, or altered request body.

Credentials belong at the end of an outbound request pipeline, after the client has decided what it is willing to send. If a component adds an API key or SSH credential before it validates the actual destination and request shape, it has already made the security decision. Everything afterward is cleanup.
That ordering matters more with autonomous coding agents. An agent can generate a plausible request, follow a link from an API response, reuse an example from a repository, or accept a redirect without understanding the consequence. None of that requires the agent to be malicious. It only requires a request path that lets untrusted input influence where an authenticated request goes.
The useful rule is simple: parse the proposed action, validate the complete action, freeze the action, establish the connection that matches it, then inject the credential at the last responsible moment. If the request changes, discard the authorization decision and start again.
Credentials make request validation a security boundary
A credential injector is not a convenience wrapper around an HTTP client. It decides which remote party receives authority to act as you. That means the validation boundary must include more than a hostname field copied from a request object.
For an HTTP action, validate at least the scheme, hostname, port, method, path, query rules, relevant headers, and body. For SSH, validate the host, port, host key trust decision, remote account, command, environment forwarding, and any file-transfer target. The details differ, but the ordering does not.
Teams often blur two separate questions:
- Can this request reach the intended service?
- Should this credential authorize this exact request?
A successful DNS lookup and a valid TLS certificate answer only part of the first question. They do not answer the second. A request to https://api.example.com:8443 is not automatically equivalent to one sent to the normal HTTPS port. A POST /v1/refunds with a bearer token is not interchangeable with a GET /v1/me, even when both land on the same host.
The mistake I see most often is a broad rule such as "this key is for api.example.com," followed by a client that accepts an arbitrary URL, adds the header, and asks the library to send it. That rule leaves too much authority in URL parsing, redirect handling, proxy behavior, and agent-controlled headers. It also makes reviews misleading. A human may approve a request described as one thing while the wire request says another.
The request validator must be the authority on both sides of that gap. It should make an explicit decision about a structured request, not search a string for a familiar domain name and hope the rest behaves.
Parse the destination into a canonical origin
A destination check should compare structured URL components, not string prefixes. The relevant identity for an HTTP origin is the scheme, host, and port. RFC 3986 defines the authority portion as optional user information, a host, and an optional port. RFC 9110 uses the origin concept for HTTP requests. Those are small definitions with large consequences.
Start by parsing the URL with a real URL parser. Reject values that your integration does not need. Do not repair malformed input into something more permissive. A validator that tries to be helpful often creates a second parser whose behavior drifts from the HTTP client.
For a typical API credential, a conservative destination rule looks like this:
accepted scheme: https
accepted host: api.billing.example
accepted port: 443 only
accepted paths: /v1/invoices/* and /v1/customers/*
userinfo: forbidden
fragments: ignored before sending, rejected in proposed actions
IP literals: forbidden unless explicitly configured
Canonicalization needs restraint. Lowercase a DNS hostname before comparison. Treat an omitted HTTPS port and port 443 as the same effective port. Ensure the parser has separated userinfo from the host. Normalize dot segments only if you then validate the normalized path, and do not decode reserved characters until you know how the client will interpret them.
Several URLs expose why prefix checks fail:
https://api.billing.example.attacker.invalid/v1/invoices
https://[email protected]/v1/invoices
https://api.billing.example:8443/v1/invoices
https://api.billing.example/v1/../admin/users
Only the first characters look familiar. Their authority or their eventual path can be different. The second URL is especially worth testing because the text before @ is userinfo, not the remote host. A browser may render it in a way that encourages a hurried reviewer to glance at the wrong part.
Internationalized domain names deserve the same caution. Choose whether the integration accepts a fixed ASCII hostname or a defined set of internationalized names. Convert and compare under one documented rule. Do not compare a display form in one place and a wire form in another.
DNS is not your authorization database. You may use DNS to connect after accepting an origin, but do not accept a destination merely because it resolves to an expected address. Shared hosting, load balancers, changing service addresses, and DNS rebinding make IP-based assumptions brittle. If you need private-network protections, apply them as an additional connection rule, not as a replacement for an origin allowlist.
Validate the connection target and the HTTP authority together
The URL, TLS server name, and HTTP authority must describe the same approved destination. If they do not, the credential injector should stop.
HTTP has more than one place where authority can appear. HTTP/1.1 has the Host header. HTTP/2 and HTTP/3 use the :authority pseudo-header. An HTTP proxy may receive an absolute-form request target containing another authority. RFC 9112 requires a client to send a Host header in HTTP/1.1, and it treats missing, repeated, or invalid Host fields as malformed. That rule exists because routing by authority is not optional decoration.
For a credential-aware client, the safest arrangement is to keep authority construction out of the agent's control. The trusted transport builds Host or :authority from the already approved URL. It does not accept a second, agent-provided routing authority. It also does not accept agent-supplied Connection, Proxy-Authorization, Transfer-Encoding, Content-Length, or Expect headers unless a narrow integration requires one and the implementation handles it deliberately.
This avoids a nasty split-brain request. Imagine a validator that approves https://api.billing.example/v1/invoices, then merges arbitrary headers from the agent. If the lower HTTP stack honors a supplied Host value, a proxy, gateway, or misconfigured server may route the request by that header. The validator approved one destination while the request reached another.
The same rule applies to proxy configuration. A corporate proxy can be legitimate, but it is a transport route, not a new authority for the credential. Keep proxy settings outside the agent's request data. Validate the final destination independently, and make the proxy behavior visible in audit records.
TLS certificate verification is mandatory for HTTPS, but certificate verification is not permission to use any credential. The client should verify the hostname from the approved URL, use that hostname for server-name indication when applicable, and reject a certificate mismatch. Do not offer a "skip verification" switch to an agent. A temporary diagnostic shortcut has a way of becoming a permanent escape hatch.
Redirects are new requests, not a continuation
An authenticated redirect is a second request with a new destination. Treating it as a transparent continuation is how credentials leave the boundary you meant to enforce.
The safest default for API clients is to disable automatic redirect following whenever a request carries credentials. Return the redirect response to the trusted request layer, parse the Location value, resolve it according to URL rules, and run the resulting request through the full validator. Only then decide whether to make another request, and only then inject a credential for that new request.
A redirect to a different origin should receive no credential from the original request. That includes a different scheme, hostname, or effective port. A redirect from HTTPS to HTTP must fail outright for a credentialed API call. A redirect from api.example.com to login.example.com is also an origin change, even if both names belong to the same company. Company ownership is not a transport rule.
The HTTP status code changes the risk. RFC 9110 specifies redirect behavior, including codes that preserve the request method and body. RFC 9700, the OAuth 2.0 Security Best Current Practice, warns that authorization servers must not use HTTP 307 when redirecting a request that might contain user credentials. The reason is plain: a client can repeat the original method and body at the new location.
That gives you a practical redirect policy:
- Reject redirects by default for credentialed machine-to-machine calls.
- Permit only a small, documented set of redirects when an integration needs them.
- Revalidate the resolved destination, method, headers, and body for every hop.
- Remove all credentials before considering the next hop.
- Set a low redirect limit and log each decision.
Do not solve this by trusting every subdomain. uploads.example.com and api.example.com may be run by different teams, use different infrastructure, or expose different attack paths. A wildcard that feels convenient during setup often outlives the reason it existed.
There is a related trap in signed requests. If an API signs the method, path, selected headers, or body digest, a redirect cannot usually preserve the signature anyway. Re-signing is appropriate only after the next request passes its own validation. A signature proves that someone with the secret signed data. It does not prove that the data still describes an approved destination.
Headers need ownership before they need filtering
Header filtering gets easier when you decide who owns each header. The request layer should own credentials and routing. The agent can own only the application headers that a specific integration permits.
Credential headers include Authorization, a vendor-specific API key header, cookies, and sometimes a signature header. Inject them after validation. Never take one from the agent, even if the agent claims it has a placeholder. A placeholder invites accidental substitution logic, and it teaches the wrong interface: the agent proposes an action, while the trusted component supplies authority.
Routing and framing headers include Host, Content-Length, Transfer-Encoding, Connection, Upgrade, and HTTP/2 pseudo-headers. Let the transport library build these. User input should not override them.
Application headers may be allowed, but only as a schema. Suppose an API accepts a customer identifier, an idempotency key, and a content type. Permit those names, validate their values, and reject everything else. Do not pass through an arbitrary header map because most calls use harmless headers. The rare header is the one that turns an ordinary request into a proxy instruction, cache variant, alternate identity, or debugging path.
Authorization needs special handling in logs as well. Log that the injector used credential reference billing-prod-readwrite, not its value or an encoded form. Redacting a header after a generic logger has already captured it is not reliable. Build a safe event from structured fields before any component serializes the request.
Custom header credentials are not less sensitive than bearer tokens simply because they use a name like X-Api-Key. If the receiving service accepts the value as authority, anyone who receives it may be able to replay it. Different header names change interoperability and logging habits. They do not change the need to validate where the value goes.
The method, path, and body define the action
A host allowlist is too broad when one credential can read data, alter data, or trigger money movement. The request shape must participate in the permission decision.
Start with the method. Permit the methods the integration needs and reject the rest. Do not describe POST as inherently dangerous and GET as safe. Many APIs expose state-changing operations behind GET endpoints, and a GET can leak private information through query parameters or logs. Method rules still matter because they make policy review concrete.
Then validate the path against route templates, not a vague prefix. A route template such as /v1/projects/{project_id}/deployments can enforce the number of segments, allowed characters in identifiers, and whether an agent can select a project outside its assigned scope. If an endpoint uses a query parameter to select an account, validate that parameter too. A correct hostname does not make /v1/accounts/other-team/export acceptable.
The body must be part of the frozen request. A validator that approves a JSON object, then lets another layer serialize or mutate it, may not authorize the bytes that leave the machine. This becomes visible with duplicate JSON keys, form encoding, multipart boundaries, floating-point conversion, and request middleware that adds fields.
A practical design has the validator produce an immutable execution plan:
{
"method": "POST",
"url": "https://api.billing.example/v1/invoices/inv_123/cancel",
"headers": {
"content-type": "application/json",
"idempotency-key": "job-7f3c"
},
"body_sha256": "4d94c2...",
"credential_ref": "billing-cancel"
}
The transport receives the plan and the prepared body bytes. It confirms the body digest before it opens the authenticated request. It derives routing headers from the URL, adds the secret from credential_ref, and sends exactly those bytes. If the digest differs, the operation fails rather than trying to guess which stage changed the request.
This approach also improves human approval. An approval card can show a plain-language action plus the canonical origin, method, route, selected account identifier, and amount or resource name. It should not ask a person to approve a raw JSON blob where a dangerous field is buried near the end.
A small validator is safer than a general policy language
The popular instinct is to build a broad rules engine: arbitrary conditions, regular expressions, variables, exceptions, and an emergency bypass. That sounds flexible until someone has to decide whether a credential can reach a redirect target with a proxy header and a JSON body assembled by an agent.
Most credential injection needs a smaller model. Each credential should have an explicit channel and a compact request contract. For HTTP, that contract names accepted origins, methods, routes, permitted headers, redirect behavior, and body constraints. For SSH, it names hosts, users, host key expectations, permitted command forms, and transfer constraints.
A compact configuration can look like this:
credential: billing-cancel
channel: https
origins:
- https://api.billing.example:443
methods: [POST]
routes:
- /v1/invoices/{invoice_id}/cancel
headers:
content-type: application/json
idempotency-key: generated
redirects: deny
body:
required_fields: [reason]
allowed_fields: [reason]
This fragment is not a complete security system. It does demonstrate the important constraint: a credential is tied to a narrow shape of action. If the next integration needs GET /v1/invoices/{invoice_id}, give it a separate route and, if possible, a separate read-only credential. Do not quietly turn the cancellation credential into a general account key.
Regular expressions deserve suspicion here. They can be useful for one field with a carefully defined grammar, but they are a poor substitute for parsing URLs, JSON, shell commands, or HTTP headers. An expression that appears to restrict a path can fail when decoding, normalization, or a downstream router interprets the same bytes differently.
Sallyport takes a deliberately smaller route for agent actions: it keeps secrets in its encrypted vault and executes HTTP or SSH actions without exposing those secrets to the agent. That separation is only useful when the action gateway validates the action before it asks the vault to use a credential.
Validation must survive the handoff to the transport
A perfect validator does not help if a later layer can alter the destination. The boundary needs a handoff that preserves what was approved.
Do not validate a mutable request object and hand the same object to middleware that can rewrite URLs, merge headers, attach cookies, follow redirects, or select a proxy from agent-controlled environment variables. Validate into a new, immutable plan. Give the transport the least expressive input you can.
The execution order should be boring and fixed:
- Parse the agent's proposed action into typed fields.
- Validate the canonical destination and allowed request shape.
- Serialize the approved payload once and record its digest.
- Build the connection using the approved scheme, host, and port.
- Inject the credential in the trusted transport immediately before transmission.
Do not inject earlier to make retry code easier. A retry is another transmission and needs the same destination and request checks. It may reuse an approved immutable plan when nothing material has changed. If a retry changes a host, route, proxy mode, method, body, or authentication scheme, it is a new action.
Connection reuse is safe only if the HTTP library keeps authority boundaries intact. A pooled connection must not let one request's authorization metadata bleed into the next request. That sounds obvious, but shared mutable header maps and poorly scoped interceptors are a common source of this class of bug.
For SSH, the equivalent failure is validating a hostname but allowing a command wrapper to pass a different ProxyCommand, agent socket, destination jump host, or remote command after approval. The trust decision must bind the entire route and execution request, not merely the first hostname visible to the agent.
Test the failures that ordinary integration tests skip
A credential injector should have tests that prove it refuses suspicious input. Happy-path tests confirm that an API call works. Refusal tests confirm that the design still means what you think it means after a library upgrade or a new agent feature.
Build a table of proposed actions and expected decisions. Include at least these cases:
- An exact approved HTTPS origin and route, which succeeds.
- A hostname suffix such as
api.billing.example.attacker.invalid, which fails. - A URL with userinfo before
@, which fails. - A valid host on an unexpected port, which fails.
- A redirect to a different origin, which fails without sending a credential.
- An unexpected
Host,Authorization, or proxy header, which fails. - A body that changes after approval, which fails the digest check.
Use a local test server that records every received request header. This is more convincing than checking a mocked request object. Your test should assert that the server at an unapproved redirect target saw no API key, no bearer token, no cookies, and no signature header. Test both redirect response codes that preserve a body and those that commonly become a GET, because library defaults vary.
Also test parser disagreement. Feed the validator and the production HTTP client the same unusual URLs, including percent encoding, empty ports, duplicate slashes, dot segments, IPv6 literals if supported, and internationalized names if supported. If they disagree about the authority or path, reject that category until you can make the behavior consistent.
Audit records should capture the validator's decision before the network call and the transport result afterward. A useful record says which credential reference was requested, which canonical origin and route were approved, whether a redirect appeared, and why a request was denied. It never contains secret material. If you cannot reconstruct why an outbound credential was used, you do not have enough evidence for an incident review.
Approval is useful only after the request is concrete
A human approval prompt can stop an agent from using a credential at the wrong moment, but the prompt has to describe a request that the system has already validated. Asking for approval first and parsing later turns the person into a weak URL parser.
Show the origin, action, method, route, and the important business fields. For a payment call, show the recipient, currency, and amount. For source control, show the repository, branch, and operation. For an infrastructure API, show the account, region, resource, and destructive effect. Keep raw secrets and unbounded request bodies out of the prompt.
Per-call approval makes sense for credentials that can cause material harm. Session approval works for repeated, low-risk calls when the session has a recognizable process identity and a short lifetime. Neither replaces request validation. A user may approve a trusted coding process, but that does not mean every URL assembled by that process deserves the same credential.
Sallyport's per-session authorization and optional per-call key approvals fit after this boundary: the app can ask a person to authorize a known agent process or a particular credential use, while the trusted action path retains the secret and records the action. The approval must cover the concrete, validated execution plan, not an agent's informal description of its intent.
The first change to make is usually not a large policy project. Disable automatic redirects for authenticated calls. Reject agent-owned routing and credential headers. Parse the destination into a canonical origin. Then bind the method, route, headers, and body before any secret enters the request. That order prevents a whole family of leaks that no amount of careful secret storage can repair.
FAQ
Is HTTPS enough before sending an API key?
No. TLS tells you that the client completed a protected connection to the server identity it verified. You still need to decide whether that server, port, path, method, headers, and body belong to the action your credential is allowed to perform.
What exactly should an API client validate in a destination URL?
Treat an origin as the scheme, host, and effective port together. Lowercase hostnames, reject userinfo and unexpected ports, normalize only rules you understand, then compare the parsed origin against an explicit allowlist.
Should an HTTP client forward Authorization on redirects?
Do not carry credentials across an origin change. The safer default is to disable automatic redirects for authenticated calls, inspect the response, validate the next URL as a fresh request, and inject credentials only if it passes.
Should an agent be allowed to set the Host header?
Usually no. Host and connection-routing headers describe the transport request, while identity headers prove something about the caller. Let the HTTP stack create routing headers and reject agent-supplied versions unless your design has a narrow, tested reason to permit them.
Why do I need to validate the request body before adding credentials?
Validation needs the request method, canonical destination, selected headers, and the actual body bytes or a cryptographic digest of them. If any of those can change after validation, your approval and your credential apply to different requests.
Are custom API key headers safer than bearer tokens?
No. A bearer token is portable by design: any receiver that gets it can often replay it until it expires or is revoked. Custom headers may make accidental disclosure less visible, but they do not change that property.
Can I allow any URL that starts with my API domain?
Do not use a prefix match as the main authorization rule. A URL parser can expose cases where similar-looking strings have different authorities, and a path prefix can match more than you intended. Parse first, then compare structured fields.
What should an authenticated outbound request audit log contain?
Log the request identity, the credential reference rather than its value, the policy decision, redirect decisions, a body digest where appropriate, timestamps, and the result. Keep the log append-only or otherwise tamper-evident if you need it for incident review.
Can an AI agent safely use credentials it never receives?
An agent can propose an action, but a trusted local component must parse and validate it before it can use a secret. The agent should receive the result, not the API key, private key, or a substitute value it can replay elsewhere.
What is the smallest safe credential injection policy?
Start with one credential and one destination family. Disable redirects, reject routing headers, allow one or two methods and paths, bind the body to validation, then add cases only when a real integration requires them.