Can URL userinfo hide your API destination?
URL userinfo can disguise an API destination. Learn how to parse, reject, display, and test userinfo safely before agents send requests.

An agent that accepts an arbitrary URL can be pointed somewhere its operator did not intend. URL userinfo makes that mistake easier because it puts a familiar hostname before an @ sign while the actual destination sits after it. If your approval screen, allowlist, or audit view treats the whole string as a host-shaped label, a request can look approved and still leave for another server.
Treat userinfo as disallowed input for outbound API actions unless you have a narrow, documented compatibility reason to support it. Parse the URL once at the boundary, reject a nonempty userinfo field before credentials enter the request, and make every later decision from parsed fields. This is not an exotic URL trick. It is ordinary syntax meeting a review surface that asks people to read too much punctuation too quickly.
The hostname is after the final at sign
In an absolute HTTP URL, the authority sits between // and the next /, ?, or #. RFC 3986 describes that authority as optional userinfo, then @, then host, then optional port. The host is therefore not whatever text appears first after //.
Consider this request:
https://[email protected]/v1/charges
A person scanning the beginning may register api.example.com and stop there. A conforming URL parser separates it this way:
scheme: https
userinfo: api.example.com
host: collector.invalid
port: 443
path: /v1/charges
The TCP connection and TLS hostname verification use collector.invalid. The string before @ does not identify the remote server. It is userinfo, a legacy part of the URL grammar once used for names and passwords.
The same issue appears in a more familiar-looking form:
https://billing.example.com:[email protected]/invoices
Everything before the @ is still userinfo, including the colon and the text after it. The remote host remains evil.invalid. A reviewer cannot safely infer destination from the first hostname-like substring in a raw URL.
Do not try to solve this by teaching reviewers to notice the character. People make this mistake during incident review, at the end of a long day, and when an agent produces many action requests. A control that depends on flawless visual parsing is a weak control.
The URL Standard used by browsers and many runtimes has the same practical result for special schemes such as HTTPS: parsed username and password fields are separate from the hostname. Exact parser behavior can differ around malformed input and escaping, which is another reason to parse with the runtime that will send the request. Do not validate with one library and execute with another that accepts a different set of strings.
A useful rule is simple: only a parsed hostname may decide where a request is allowed to go. Raw URL text is evidence, not authority.
Userinfo creates a review problem before it creates a network problem
The network client usually knows where it is going. The failure happens earlier, when a person or a control approves the wrong representation of that destination.
A typical agent flow has several places where raw text can leak through: the tool call argument, an approval card, a session journal, an error message, and a notification. If one of those views says Calling api.example.com because it extracts text before @, it tells an operator something false. If another view logs the full string but truncates it after a fixed width, the actual host can disappear completely.
This matters even when the agent has no credential for the target. An outbound request can carry user data, a signed request body, a bearer token chosen by a broad rule, or simply reach an internal address that should never receive agent traffic. The credential story gets attention because it is concrete. Destination integrity needs the same discipline.
The distinction people often blur is between URL display safety and URL transport safety. Escaping @ in an HTML view may make a page less confusing, but it does not determine where an HTTP client connects. Conversely, a parser may make the network call correctly while a badly designed approval view still encourages a person to approve the wrong host. You need both a correct transport decision and an honest display.
Do not replace @ with an innocuous character in the stored request and continue. That hides the input that caused the denial and makes later investigation harder. Preserve the original string as raw input, mark the request as rejected, and record the parsed reason without writing embedded credentials into a readable journal.
The display should lead with a separate destination field, such as Host: collector.invalid, and place the original URL below it. That ordering turns a punctuation puzzle into a direct statement. It also gives a reviewer a stable field to compare with the requested credential or the intended integration.
Rejecting userinfo is safer than repairing it
For an agent action gateway, the clean default is to reject every outbound HTTP URL whose parsed username or password field is nonempty. Most API integrations already send credentials in request headers or use a credential injector. Supporting URL userinfo adds attack surface without solving an ordinary API need.
The validation order matters. Parse the original string first. Reject malformed URLs, unsupported schemes, and userinfo before host allowlisting, approval prompts, redirects, DNS resolution, or credential selection. That sequence avoids displaying a request as eligible when you will later discard part of it.
This pseudocode expresses the rule:
u = parse_absolute_url(raw_url)
if u.scheme not in {"https", "http"}:
deny("unsupported scheme")
if u.username != "" or u.password != "":
deny("URL userinfo is not accepted")
host = normalize_hostname(u.hostname)
if host == "":
deny("missing hostname")
if not destination_is_allowed(u.scheme, host, u.port):
deny("destination is not allowed")
send(u)
The parser must return structured fields. Splitting on @, stripping a prefix, or searching for a hostname substring all fail on ordinary variations. An authority can contain more than one @ in raw input. A parser decides which delimiter is syntactic and whether earlier characters belong to userinfo. It also handles bracketed IPv6 literals, explicit ports, percent encoding, and empty components more consistently than handwritten string logic.
Rejecting userinfo gives the caller a clear correction: use https://api.example.com/path, then supply HTTP authentication through the designated credential path. It does not leave the caller guessing whether you silently changed https://name@host into https://host.
There is one compatibility case worth acknowledging. Some old URLs embed Basic authentication as https://name:secret@host/path. If a migration must handle those URLs, do it in a one-time import path that extracts the credentials into protected storage, confirms the parsed host, and deletes the source string from the import record where policy permits. Do not allow the runtime action API to keep accepting them forever. Temporary compatibility code has a habit of becoming permanent attack surface.
Host allowlists need parsed labels, not friendly-looking strings
A destination allowlist should compare normalized parsed hostnames, not run a substring search over the raw URL. The rule raw_url.includes("api.example.com") accepts both https://[email protected] and https://api.example.com.evil.invalid. Neither is a request to api.example.com.
Exact host matching is the least surprising rule. If an integration needs only api.example.com, allow that name and reject every other hostname. If it genuinely needs subdomains, compare DNS labels: permit example.com and names ending in .example.com, but reject badexample.com and example.com.evil.invalid.
A clear implementation shape looks like this:
function allowedHost(host, root) {
const h = host.toLowerCase().replace(/\.$/, "")
const r = root.toLowerCase().replace(/\.$/, "")
return h === r || h.endsWith("." + r)
}
That code assumes the URL parser has already supplied a hostname and the caller has already rejected userinfo. It should not receive a full URL. Keeping those responsibilities separate prevents a later caller from passing an authority string that contains a port, a username, or an @ sign.
Internationalized domains need a decision too. Browsers commonly serialize hostnames in ASCII using IDNA processing, while a user may see Unicode text. Use the same canonical form your request client uses for comparison, and show both the canonical host and a readable form when they differ. Do not claim two strings identify the same domain because they look similar in a proportional font.
IP addresses deserve their own rule. A hostname allowlist does not automatically make an IP literal safe, and DNS resolution after approval can change the address a hostname reaches. If your threat model includes access to local services, decide explicitly whether private, loopback, link-local, and IPv6 local addresses are allowed. Rejecting userinfo is necessary, but it does not solve server-side request forgery by itself.
Ports also carry meaning. https://api.example.com:8443 may be a valid partner endpoint or an unexpected administration service. Record the effective port and include it in approval decisions where the integration restricts one. A host label alone does not describe the full network destination.
Redirects must pass through the same gate
A permitted initial URL does not make every redirect target permitted. HTTP redirects are new destination instructions supplied by the remote server, and an agent gateway should parse and authorize each one before following it.
Suppose an agent requests https://api.example.com/export. That host returns a 302 response with this location:
https://[email protected]/download?id=42
A client that follows redirects automatically will next connect to receiver.invalid. If the gateway approved only the first URL, its allowlist and approval screen no longer describe the network action that occurred.
Handle redirects as a loop with a limit chosen for your client. For every location value, resolve a relative reference against the current approved URL, parse the resulting absolute URL, apply the same scheme, userinfo, host, port, and address rules, then decide whether to proceed. Record both the source response and the parsed redirect destination.
Do not forward credentials across hosts by default. HTTP client libraries differ on whether they retain an Authorization header after a cross-host redirect, and custom headers can behave differently again. The safest gateway behavior associates a credential with a specific approved destination and constructs a new outbound request only after the redirect destination passes authorization. A redirect from one vendor-owned host to another may be expected, but it should be an explicit rule rather than an accident of library defaults.
Method handling needs attention as well. A 303 response commonly changes a later request to GET, while 307 and 308 preserve the method and body. If an agent posts a sensitive body, a preserved-method redirect can send it elsewhere. Record the method used on every hop and show the final destination in the action result.
A client can also be configured not to follow redirects. That is a sensible choice for narrow API tools. Return the redirect response to the agent and require it to ask for the next destination explicitly. This produces another approval event, but it gives the operator a clean point to assess a host change. For broad HTTP support, automatic redirects are acceptable only when each hop crosses the same gate.
Credentials must be selected after destination validation
The dangerous ordering is easy to describe: pick a credential because the raw URL contains a familiar service name, then parse the URL, then send the request. An @ trick can turn the familiar text into userinfo while the selected secret travels to an attacker-controlled host.
The safer ordering is equally plain. First parse and validate the URL. Next authorize its scheme, host, port, and any redirect state. Only then find a credential bound to that approved destination and inject it into the outgoing request. Keep that secret out of tool arguments, agent memory, and return values.
This also solves a less dramatic but common configuration error. A credential associated with api.example.com should not automatically go to uploads.example.com, even if both names sit under the same parent domain. Different hosts often have different ownership, TLS termination, logging, or permission scopes. Start with exact host binding. Broaden the match only when the integration documents why it needs more.
Header injection is preferable to URL userinfo because it separates destination from authentication. A request record can say that an authorization header was injected without storing its value. The agent receives the response needed to continue its work, not a reusable secret.
Sallyport follows that separation by keeping API and SSH credentials in its encrypted vault and executing the outbound action without exposing those credentials to the agent. For any gateway with that model, rejecting userinfo before credential lookup closes the gap between an operator's intended destination and the host that receives the request.
Do not trust a request header supplied by the agent to identify its destination either. The Host header, HTTP/2 authority, URL host, proxy configuration, and TLS server name can interact in client-specific ways. A gateway should own the connection settings and derive them from the validated parsed URL. If you allow custom headers, treat them as request content, not as permission to rewrite routing.
Approval cards should show the parsed destination first
An approval card should answer three concrete questions without asking its reader to reconstruct a URL: which process asked, what operation will run, and which host will receive it. Put the parsed hostname and port in a dedicated destination line. Put the HTTP method and path nearby. Show the original URL as supporting evidence, not as the only destination signal.
For the malformed-looking request used earlier, a useful card would say:
Process: signed agent process
Action: POST /v1/charges
Host: collector.invalid:443
Result: blocked because URL userinfo is present
Input: https://[email protected]/v1/charges
It should not display api.example.com as a badge derived from the left side of the authority. Nor should it say only External HTTP request, which gives a person no meaningful basis for a decision.
Per-session approval and per-call approval solve different human problems. A session approval says that a particular running process may use a gateway for its lifetime. A per-call approval says that an especially sensitive credential or action needs a fresh human decision. Neither replaces basic URL validation. A process you trust can still be manipulated by an untrusted issue comment, package metadata field, or generated configuration file.
Sallyport's decision ladder keeps a locked vault as an absolute stop, then uses session authorization and optional per-secret confirmation. That structure works best when malformed destinations fail before they reach an approval card, because an operator should not have to decide whether a piece of URL punctuation changed the endpoint.
Be specific in denial messages. Userinfo is not allowed in outbound URLs tells an agent developer what to fix. Invalid request causes retries, ad hoc escaping, and pressure to weaken validation. Avoid echoing a password if the parser extracted one. The message can name the prohibited component without reproducing its contents.
Tests should use deceptive inputs, not only happy URLs
A validation test suite needs examples designed to fool human readers and simplistic string checks. Passing https://api.example.com/v1 proves almost nothing about the boundary where an agent can submit arbitrary text.
Start with cases like these and assert the parsed host, decision, and reason:
ALLOW https://api.example.com/v1 host=api.example.com
DENY https://[email protected]/v1 reason=userinfo
DENY https://name:[email protected]/v1 reason=userinfo
DENY https://api.example.com.evil.invalid/v1 reason=host
DENY https://api.example.com:444/v1 reason=port
DENY https://[::1]/v1 reason=address
Add a redirect fixture. Have an allowed test server issue a redirect whose location contains userinfo and assert that the client records a blocked second hop without sending a request to the destination server. This catches the frequent mistake where initial URL validation lives in one code path and redirect handling lives inside a library callback.
Test percent encoding deliberately, but do not assume every encoded @ is equivalent. In many parsers, %40 in a path remains path data while an actual @ in the authority acts as a delimiter. Feed the exact raw string into the parser you use in production and assert its fields. The important invariant is not a homemade decoding rule. It is that a nonempty parsed userinfo field cannot reach the sender.
Also test log and approval rendering. A security control can make the correct denial and still create a bad operational record if the display truncates the actual host or exposes parsed password text. Snapshot tests for destination lines are worth having because visual regressions often arrive through harmless-looking design changes.
Finally, test the audit chain and revoke path around a denied call. A denial should be visible enough for investigation, yet it should never include injected secrets. The useful record is the raw request under appropriate access controls, parsed scheme and host, decision reason, calling process identity, and the fact that no outbound action occurred.
URL syntax is not a policy language
Some teams respond to URL edge cases by adding a growing pile of exceptions: permit a username for this vendor, accept a special port for that environment, trust a redirect only when a header looks right, and patch a string matcher for each incident. That approach feels flexible because it avoids saying no to a strange request. It also creates rules that nobody can reliably review.
Keep the rule small. Outbound requests have a parsed destination. Userinfo is rejected. The allowed scheme, host, port, and address class are explicit. Redirects re-enter the same checks. Credentials are selected only after the destination passes. Each decision produces a record that states the parsed host plainly.
That rule will reject a few old URLs that a browser might accept. Good. An autonomous agent does not need every historical affordance of a browser address bar. It needs a narrow interface that makes the action, destination, and credential relationship hard to confuse.
If you need to change that interface, make the exception visible as a named capability with tests and an expiration decision. Do not bury it in URL cleanup code. The first hostile input will find the difference between text that looks like a host and the host your client actually contacts.
FAQ
Can an @ sign in a URL change the destination host?
Yes. In a URL such as https://[email protected]/path, the destination host is evil.example, not api.example.com. The text before @ is userinfo, so accepting it in agent-supplied URLs creates an easy route for human review errors.
Is URL userinfo valid syntax?
It is valid URL syntax, although many API clients have no reason to accept it. RFC 3986 permits optional userinfo in an authority, but permission in a grammar is not a reason to carry it through an agent action boundary.
Should an API gateway reject URLs with userinfo?
Reject it at the boundary unless you have a tightly defined compatibility need. Do not strip it and continue silently, because that changes the request the caller supplied and leaves a misleading record.
Does user:[email protected] send traffic to user?
No. https://user:[email protected]/v1 has hostname api.example.com; user:pass is userinfo. A parser exposes these fields separately, and security checks should use the parsed hostname rather than the raw string.
Should I put Basic authentication credentials in a URL?
Basic authentication belongs in an HTTP Authorization header, not embedded in a URL. URL credentials leak into logs, histories, copied commands, and error messages far more easily than headers do.
Do redirects make URL userinfo validation harder?
Every redirect target needs the same parsing and authorization checks as the original URL. Checking only the first URL lets a permitted public endpoint redirect an agent to an unapproved host.
Is api.example.com.evil.example the same as api.example.com?
No. api.example.com.evil.example is a subdomain of evil.example, while [email protected] actually targets api.example.com. Parse the host first, then compare domain labels according to your explicit allowlist rule.
How should I validate an outbound API URL?
Avoid regexes over the whole URL. Use a standards-compliant parser, reject userinfo explicitly, require HTTPS where appropriate, normalize the parsed hostname, and compare it against the allowed hosts or domain suffixes.
What should an audit log record for an agent HTTP request?
A good audit record keeps the raw input for investigation and stores parsed fields separately: scheme, hostname, port, path, redirect target, and decision. Display the parsed hostname prominently so a reviewer does not have to mentally parse punctuation.
How do I test an agent tool for disguised URL destinations?
The fix is usually a small validation rule, but it belongs before approvals and credential injection. Add test cases for @, percent-encoded delimiters, multiple @ characters, redirects, mixed case hosts, and trailing dots so a later refactor does not reopen the gap.