fix: reject control characters in header names#140
Conversation
Headers.Builder validated header values for CR/LF but never validated header names: name handling only lower-cased and trimmed surrounding whitespace, so an interior \r, \n, NUL, or other control character survived into the model layer. Downstream the two reference transports diverged on such a name — the OkHttp transport threw an unchecked IllegalArgumentException out of execute() (escaping its IOException contract and bypassing the retry pipeline), while the JDK transport silently dropped the header. It was also a header-injection surface for attacker-controlled names. Add validateName beside validateValues, invoked from the String-based add/set before the name is stored. It rejects blank names and any character in the C0 control range (0x00-0x1F, covering CR/LF/NUL) or DEL (0x7F). The typed (HttpHeaderName) overloads already carry pre-validated token names, so they are unaffected. The policy deliberately stops at control characters rather than RFC 7230's full tchar allow-list, so it does not reject non-ASCII names that some transports accept while still closing the splitting/injection surface. Correct the OkHttp and JDK transport-adapter comments that previously claimed names were validated upstream; they now describe the actual name and value guarantees.
|
This adds a IssuesName validation is missing on the The new guard is only called from the two okhttp adapter comment overstates the guarantee — The updated comment now asserts names reject control characters upstream so No test exercises the typed overload for name validation — All 13 new tests go through the |
Problem
Headers.Buildervalidated header values for CR/LF but never validated header names. Name handling only lower-cased and trimmed surrounding whitespace, so an interior\r,\n, NUL, or other control character passed through the model layer intact.Downstream, the two reference transports then diverged on such a name:
addHeader(name, value)with no guard. OkHttp rejects an illegal name with an uncheckedIllegalArgumentException, which escapedexecute()despite its@Throws(IOException)contract and bypassed the transport's IOException handling / retry pipeline.header(name, value)intry/catch(IllegalArgumentException)and silently dropped the offending header.So the same SDK-level request produced two different observable outcomes depending on transport. It was also a header-injection surface for attacker-controlled names.
Change
validateNamebesidevalidateValuesinHeaders.Builder, invoked from theString-basedadd/setbefore the name is stored. It rejects blank names and any character in the C0 control range (0x00–0x1F, which covers CR/LF/NUL) or DEL (0x7F).HttpHeaderNameoverloads carry pre-validated token names, so they are unaffected.The policy deliberately stops at control characters rather than RFC 7230's full
tcharallow-list, mirroring the conservative CR/LF-only stance already used for values: it closes the splitting/injection surface (control characters are illegal in every transport) without rejecting non-ASCII names that some transports accept.Tests
Added coverage in
HeadersTestfor CR, LF, CRLF, NUL, and DEL injection in names via bothaddandset(single- and list-value overloads), a blank-name rejection, a message-naming check, and an accepted-name case (including whitespace trimming).Gated build (scoped,
--no-daemon)./gradlew :sdk-core:test :sdk-core:ktlintCheck :sdk-core:detekt :sdk-core:apiCheck— passed./gradlew :sdk-transport-okhttp:compileKotlin :sdk-transport-okhttp:ktlintCheck :sdk-transport-okhttp:detekt :sdk-transport-jdkhttp:compileKotlin :sdk-transport-jdkhttp:ktlintCheck— passed (detekt is not run on the JDK 11 module)No public API change (
validateNameis private), soapiCheckpasses with noapiDump.Closes #114