Refuse a header value that cannot be sent, instead of reporting an unreachable agent - #470
Open
kevin9327 wants to merge 1 commit into
Open
Refuse a header value that cannot be sent, instead of reporting an unreachable agent#470kevin9327 wants to merge 1 commit into
kevin9327 wants to merge 1 commit into
Conversation
…reachable agent `new Headers()` refuses a line break, a NUL and any code point above U+00FF, and it refuses them by throwing a TypeError from inside `fetch`. Neither of the two places that take a header value from the agent form looked, so that throw arrived somewhere that reads as something else: on the connection test it lands in the catch written for a dead host, and on the stored key it lands on every run. Both now check the value before accepting it, and name the kind of character in the way without ever echoing the value. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
kevin9327
requested review from
MikeRyanDev,
davidmckayv,
guidovizoso and
tylerslaton
as code owners
September 9, 2026 22:54
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Paste a key into an agent's key box with a line break somewhere in the middle — which is what a long
key copied out of a wrapped terminal line brings with it — and press Test connection against an
agent that is running perfectly well. What comes back is:
Measured, against a real
Bun.serveagent on loopback and a header value ofBearer abc\ndef: theagent counted zero requests. It was never dialled. The person is now looking at their tunnel and
their firewall over a value in the box they had just filled in.
Save the same key instead of testing it and it is quieter and worse. The form says saved, the value
is encrypted and stored, and every turn that Bot takes afterwards fails on it.
Why
new Headers()refuses a line break, a NUL, and any code point above U+00FF, and it refuses them bythrowing a
TypeErrorfrom insidefetch— not by anything this deployment decides. Measured on Bun1.3.14, the same probe that produced the numbers below:
Neither of the two places that take a header value looked at one:
parseConnectionHeaders(server/src/agents/routes.ts) checks the name against/^[A-Za-z0-9-]+$/, refuses__proto__, caps the map at 32 entries and the value at 4096characters — and then accepts any string. Its own docstring says it exists because "an array
value, a nested object, or a
__proto__key travelled into the network call and threw aTypeError 500". A value that throws the same TypeError was the case it did not cover.
parseAgentInput, a few lines above it, checks the header name the same way and stores thevalue as it arrives.
On the connection test the throw travels out of
createAgentFetch, intotestAgentConnection'scatch — which exists for a dead host and a typo'd address — and is answered 200 with the sentence
above. On the stored key nothing catches it at all until the next run.
The other spelling that does this is not a line break: a hyphen copied out of a document has often
been turned into an en dash, which is U+2013, and in a password box it looks like a hyphen.
The fix
One helper, used by both. It returns what kind of character is in the way and never the value —
this is asked of a credential on one of the two paths, and one character of a secret in an error
message is one character too many.
Refused rather than stripped. A key with a line break in it is not a key with a line break removed,
and silently sending a different value than was pasted is how somebody spends an afternoon on a 401.
Measured
bun test server/tests/agent-test-connection-headers.test.tsorigin/mainwith only the new tests applied: 20 pass, 6 fail.rejects a newline in the value,rejects a carriage return in the value,rejects a NUL in the value,rejects a character above Latin-1— all four returnok: true.a key that cannot be sent as a header is refused—parseAgentInputreturnsok: true.is refused before anything is dialled— the route answers 200, not 400, carrying the"could not reach that address" reason.
Four of the new cases are the guard against over-correcting, and pass before and after: a tab, an
inner space, a Latin-1 accent, and a value made of punctuation are all things
new Headers()accepts, so refusing any of them would be taking away a header somebody's agent really wants. So are
the eleven cases that were already there.
Also run, all green:
bun test server/tests/agent-routes.test.ts server/tests/agent-endpoint.test.ts server/tests/agent-connection-live.test.ts— 106 pass, 0 failbun run --filter server typecheck— exit 0bunx biome checkon both changed files — cleanNote on the CHANGELOG
A deployment behaves differently afterwards, so there is an entry. It is inserted at the top of
## Unreleased, which is where every entry goes, so it will conflict on that one line with any otherPR open against the same anchor. Happy to rebase whenever it suits you.