Skip to content

fix(runtime): reject CR/LF header names/values in the curl repair hop - #14

Merged
tarwin merged 2 commits into
tarwin:mainfrom
slabbdev:fix/curl-fetch-header-crlf
Sep 28, 2026
Merged

tarwin merged 2 commits into
tarwin:mainfrom
slabbdev:fix/curl-fetch-header-crlf

Conversation

@slabbdev

Copy link
Copy Markdown
Contributor

Fixes #11 (section 2 — the curl repair hop).

What

curlFetch built its -H arguments from raw header names and values. curl puts an embedded CRLF in an -H argument on the wire as extra request lines (verified with the system curl 8.7.1 against a local listener), so a page-supplied tiny.fetch({ headers: { 'X-Evil': '1\r\nX-Injected: yes' } }) reached the server with forged headers whenever the request routed through the repair hop (root-path URLs — the bug-A case — or any URL the native fetch throws on, bug B).

WHATWG fetch rejects CR/LF/NUL in header names and values with a TypeError; this makes the curl hop enforce the same rule, so both fetch paths agree and a header can't smuggle request lines through the repair step.

Verification

Under tjs (0.42.0 checkout, macOS 26), request to a path-/ URL with the header {'X-Evil': '1\r\nX-Injected: yes'}, server capturing the raw request:

  • before: fetch() resolved 200 and the wire carried X-Evil: 1 followed by X-Injected: yes as a separate header
  • after: fetch() rejects with TypeError: Invalid header value before curl is spawned; nothing reaches the server

(Plain headers keep flowing unchanged — the check only fires on CR/LF/NUL.)

Note

One thing I could not verify from this repo: whether txiki's own native fetch validates header values the same way. If it doesn't, the same check may be worth lifting into doFetch so both paths are covered at the page boundary too — happy to add it here or as a follow-up.

slabbdev and others added 2 commits September 27, 2026 00:43
curlFetch built -H arguments from raw header names and values, and curl
puts an embedded CRLF on the wire as extra request lines (verified with
the system curl 8.7.1 against a local listener): a page-supplied
tiny.fetch({ headers: { 'X-Evil': '1\r\nX-Injected: yes' } }) on a
root-path URL — or any request the native fetch throws on — reached the
server with forged headers. WHATWG fetch rejects CR/LF/NUL in header
names and values with a TypeError; enforce the same rule here so both
fetch paths agree and a header can't smuggle request lines through the
repair hop.

Verified under tjs: before the fix the injected header lands on the
wire; after, fetch() rejects with 'Invalid header value' before curl is
spawned.
…uded

txiki 26.6.0's native fetch also puts a CRLF in a header value on the
wire as a separate header, so the curl-hop check alone left the common
path open. Validate once at the top of fetchRepaired (and again in
curlFetch): names must be RFC 9110 tokens — a ':' in a name made curl
send a different header, Host included — and values reject CR/LF/NUL.
Also: [[k, v]] header arrays are read as pairs (Array.forEach made them
index/pair), and empty values reach curl as 'Name;' instead of 'Name:',
which curl reads as remove-this-header.

Refs tarwin#11

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tarwin
tarwin merged commit a104ce0 into tarwin:main Sep 28, 2026
@tarwin

tarwin commented Sep 28, 2026

Copy link
Copy Markdown
Owner

Thanks @slabbdev with some small changes this has been merged and will be in the next release

tarwin added a commit that referenced this pull request Sep 28, 2026
Changelog (md + site) for the debug.get gate fix, fetch header
validation and Info.plist escaping, crediting @slabbdev; skill version
strings bumped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@slabbdev
slabbdev deleted the fix/curl-fetch-header-crlf branch October 3, 2026 15:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security review: debug.get bypasses the api gate; curl repair hop forwards CR/LF headers to the wire

2 participants