Commit b5219f5
Harden HTTP.jl security defaults for v2.4.0 (#1316)
* Bound HTTP/2 server concurrent streams and mitigate Rapid Reset
The HTTP/2 server advertised an empty initial SETTINGS frame, leaving
SETTINGS_MAX_CONCURRENT_STREAMS effectively unlimited, and the HEADERS
path allocated per-stream state, a send-window entry, and a
Threads.@Spawn handler with no check on the open-stream count. RST_STREAM
was un-rate-limited, so a single connection could drive unbounded stream
state and handler-task creation (CVE-2023-44487 "Rapid Reset" /
unbounded-stream DoS). Separately, the new-stream guard used
`hf.stream_id < max_stream_id`, which let a client reuse the highest
closed stream id once it was cleaned up, including after GOAWAY.
Add a configurable per-connection concurrent-stream cap
(`max_concurrent_streams`, default 100, threaded through Server/listen!/
serve!/serve; values <= 0 disable it for backward compatibility). When
the cap is positive it is advertised via SETTINGS_MAX_CONCURRENT_STREAMS
and enforced on incoming HEADERS: a new stream over the cap is refused
with RST_STREAM(REFUSED_STREAM) without allocating state or spawning a
handler. The refusal path still drains and HPACK-decodes the (possibly
multi-frame) header block so the shared decoder stays in sync.
Track peer-initiated stream resets; once they exceed
`max_concurrent_streams + 100`, send GOAWAY(ENHANCE_YOUR_CALM) and stop
accepting new streams (counting only the first OPEN->RESET transition per
stream). Require new client-initiated stream ids to be strictly greater
than every previously-seen id, treating reuse of a closed id as a
connection error.
The new-stream policy is factored into a pure
`_h2_server_new_stream_decision` helper for offline unit testing. Tests
cover the decision helper, the SETTINGS advertisement, REFUSED_STREAM
enforcement, HPACK resynchronization after a refused stream, and
stream-id monotonicity (reuse -> connection error).
Reported to the JuliaLang security team through Anthropic's Coordinated
Vulnerability Disclosure program and tracked as JLSEC-2026-611
(Anthropic references: ANT-2026-7CVAC8Q1, ANT-2026-08Y5Y8H4,
ANT-2026-9W57Y1XN, ANT-2026-7393V1MS).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit d61bf13acdd2050bd4de9826e179e68d19c49e98)
* Harden fileserver request-path mapping against separator/absolute segments
The static file server decoded the request path, split it on '/', and rejected only segments exactly equal to '.' or '..'. Because URL-decoding runs before the '/' split, an encoded backslash (%5c), a drive specifier ("C:\..."), or a UNC prefix ("\\host\share") survived inside a single segment and passed validation. On Windows joinpath then honors '\' as a separator and treats drive/UNC segments as absolute, discarding the configured document root, enabling arbitrary file read and outbound SMB/NTLM via UNC paths.
Add _is_unsafe_request_path_segment(segment), which rejects any decoded segment that is '.'/'..', contains a path separator ('/' or '\'), contains a colon (Windows drive specifier / alternate data stream), or is absolute (isabspath). The '\' and ':' checks are explicit and platform-independent so Windows drive/UNC/backslash forms are rejected even on POSIX hosts, making the hardening uniform and testable everywhere. _decoded_request_path_segments now validates each split segment with this helper and throws ArgumentError on any unsafe segment (callers map this to 400).
Add a defense-in-depth backstop in _join_request_path: after joining the validated segments onto the root, compute normpath and require the result to equal the normalized root or start with root plus the path separator, otherwise reject. The separator suffix prevents a sibling like "<root>_evil" from matching. normpath/startswith are used (rather than realpath) to keep the check deterministic and offline, matching the existing code's avoidance of realpath.
Add a regression test ("HTTP fileserver Windows backslash path traversal") covering the segment validator, the decode-and-split path, the join+containment backstop, and an end-to-end fileserver check that the encoded-backslash, drive, and UNC traversal requests return 400 while a normal request is still served.
Reported to the JuliaLang security team through Anthropic's Coordinated
Vulnerability Disclosure program and tracked as JLSEC-2026-612
(Anthropic references: ANT-2026-VHDP7ANW).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit eb6f34d24d65cebd61ec5a942d992824d685ffc3)
* Scope redirect credential forwarding to the full request origin
Redirect handling previously decided whether to retain credential-bearing
headers (Authorization, Cookie, Proxy-Authorization, etc.) by comparing only
the hostname, ignoring scheme and port. As a result an https->http downgrade
or a same-host different-port redirect was treated as same-origin and replayed
credentials over plaintext or to a different service. Additionally, per-call
cookies= cookies were captured once and re-appended by _cookie_header on every
hop even after the Cookie header was stripped, and the TLS verification host
was pinned to the original host across redirects because request() always
supplies an auto-derived server_name.
- _should_copy_sensitive_headers_on_redirect now takes initial/redirect secure
flags, returns false on any scheme change, splits host and port (falling back
to the scheme default port so bare-host and host:default-port compare equal),
returns false on any port change, and only then applies the existing
domain-or-subdomain host check. Callers in the HTTP and WebSocket redirect
loops pass the scheme flags they already track for Referer handling.
- On any cross-origin hop the loops now also reset the explicit-kwarg cookies
vector to empty so _cookie_header no longer re-attaches them; the host-scoped
cookie jar remains active.
- The unreliable explicit_server_name flag is replaced with a check that treats
server_name as user-pinned only when it differs from the host auto-derived
from the initial URL; otherwise the SNI/verification host is recomputed from
the redirect target on every hop so TLS verifies the host actually dialed.
Adds helper unit tests for the scheme/port/host origin semantics and an offline
two-listener integration test proving a cookies= secret reaches the original
origin but not a cross-origin (different-port) redirect target.
Reported to the JuliaLang security team through Anthropic's Coordinated
Vulnerability Disclosure program and tracked as JLSEC-2026-613
(Anthropic references: ANT-2026-5F0FFVVR, ANT-2026-5HZS0066,
ANT-2026-HTKXQYJX, ANT-2026-K7VHJB7S, ANT-2026-MBWGTHMA,
ANT-2026-PW5H10EB, ANT-2026-R3BBBRAW, ANT-2026-WB9V4R8Q,
ANT-2026-8SP7TV55, ANT-2026-GM4FVXDB).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit f1ed18df97a642256bafdc58006fafe2c84e638c)
* Enforce scheme/host/port in default WebSocket Origin check
The default WebSocket Origin validator (_origin_allowed_default) only enforced the host component of the same-origin tuple. It never read the parsed Origin's scheme (parsed.secure), and when the request Host header carried no explicit port (the norm for default-port 80/443 servers, where browsers omit the port) it fell through to a hostname-only comparison, discarding the Origin's port. As a result a server at wss://example.com accepted cross-scheme/cross-port origins such as http://example.com or http://example.com:8080, letting an attacker who controls another port or scheme on the same hostname open an authenticated WebSocket from a victim's browser using the victim's cookies.
Rework _origin_allowed_default(request) into _origin_allowed_default(request, server_secure::Bool=false). It now (1) requires the Origin's scheme to match the server transport via parsed.secure == server_secure, rejecting http/ws origins on an https/wss server and vice versa, and (2) compares the Origin's effective port against the server's effective port. The server host/port is derived from the Host header with a new helper _split_host_with_default_port(value, default_port), which substitutes the server scheme's default port (443 when secure, else 80) when the Host header omits a port and safely handles bracketed IPv6 literals instead of blindly splitting on ':'. The Origin's parsed.address already carries a port (defaulting to the Origin scheme's 80/443 when implicit). Final acceptance requires lowercase host equality and port equality, with scheme equality enforced earlier. The server_secure flag is plumbed from all upgrade call sites: _origin_allowed and the _upgrade_response/server convenience method use server.tls_config !== nothing, and the manual upgrade(stream) path uses conn isa TLS.Conn. The empty-Origin allowance, cross-host rejection, and malformed-Origin rejection are preserved; server_secure defaults to false for backward compatibility.
Add a regression testset covering same-origin acceptance, cross-port and cross-scheme rejection on default-port servers (the previously mishandled case), cross-host rejection, IPv6 origins, and malformed-Origin rejection for both ws/http and wss/https transports.
Reported to the JuliaLang security team through Anthropic's Coordinated
Vulnerability Disclosure program and tracked as JLSEC-2026-614
(Anthropic references: ANT-2026-DXGTQBSK, ANT-2026-X2Q1J9M4).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 63d17cc6272f11da6614207373828e14a5fc506a)
* Enforce Secure/prefix protections on the cookie-jar store path
setcookies! previously stored every parsed Set-Cookie after only checking that the response scheme was http or https, with no symmetric protection to the read path (shouldsend, which already withholds Secure cookies from non-secure requests). A plaintext (http) origin could therefore plant a Secure cookie, plant a __Secure-/__Host--prefixed cookie, or overwrite/delete (via Max-Age=-1) an existing Secure cookie set over https, enabling cookie fixation against hosts that mix http and https.
Add three store-path checks in setcookies!, per RFC 6265bis 5.4/5.6:
- Drop any cookie carrying the Secure attribute that arrives over a non-secure scheme.
- Enforce the __Secure- (Secure + https origin) and __Host- (Secure + https + Path=/ + no Domain attribute) name prefixes, evaluated on the raw Path/Domain attributes before normalization. The prefix comparison is ASCII case-insensitive (new has_prefix helper) to match RFC 6265bis 4.1.3.1/4.1.3.2 and prevent case-variant evasion such as "__host-".
- When the origin is non-secure, refuse to overwrite or delete an existing stored cookie of the same domain;path;name identity that has the Secure attribute.
Behavior over https is unchanged. Add regression tests covering Secure-over-http rejection, prefix enforcement (including case-variant prefixes), and clobber/delete protection.
Reported to the JuliaLang security team through Anthropic's Coordinated
Vulnerability Disclosure program and tracked as JLSEC-2026-615
(Anthropic references: ANT-2026-9G63DER3, ANT-2026-GY6HYQTG,
ANT-2026-ZK96ACV8).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit c452a62eb7e789e65d9f6090bd520ba94fdd6ca2)
* Validate HTTP/1 client request start line for CR/LF/control bytes
The HTTP/1 client serialized request.method and request.target (and, in forward-proxy absolute-form, the host) verbatim onto the wire via print(), with no CR/LF/CTL filtering. The only target validator, _validate_request_target!, was wired solely into the server parse path. A caller passing an attacker-influenced URL or method to the client could embed \r\n in the method, path, or query to inject arbitrary request headers, or \r\n\r\n to smuggle a second pipelined request onto a pooled keep-alive (or proxy-forwarded) connection. The HTTP/2 client already rejected CR/LF in :path, so this was an HTTP/1-specific omission.
Add _validate_request_start_line!(method, target, host=nothing) in src/http1.jl, placed next to _validate_request_target!. It validates the method against the RFC 7230 token grammar via the existing _valid_header_field_name (which excludes space, CR, LF and other control bytes), delegates target validation to _validate_request_target! (which rejects control bytes and handles origin-form, asterisk-form, absolute-form and CONNECT authority-form), and, when a host is supplied, rejects it if it contains a control byte. Reject with a ParseError, matching the server-side behavior rather than silently rewriting a caller's intended target.
Wire the guard into all three HTTP/1 wire start-line writers before any bytes are emitted: _write_start_line! and _append_start_line! in src/http1.jl, and the forward-proxy absolute-form _write_start_line!(io, request, plan) in src/http_transport.jl. The proxy variant validates request.method, request.target and the forward address separately before they are concatenated into the absolute target, so a CRLF-laced host cannot be smuggled through the proxy. This covers origin-form, CONNECT authority-form (tunnel establishment), asterisk-form, websocket, and proxy-forward absolute-form write paths. Add a regression testset in test/http1_wire_tests.jl covering validator-level rejection (method/target/host CR, LF, NUL, embedded space), end-to-end origin-form and proxy-forward write rejection, and benign no-false-positive serialization.
Reported to the JuliaLang security team through Anthropic's Coordinated
Vulnerability Disclosure program and tracked as JLSEC-2026-616
(Anthropic references: ANT-2026-5W0B63VQ, ANT-2026-8WT2MXD5,
ANT-2026-23PZ587V, ANT-2026-P0SGS9PG).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit eaabf6d9fd6fedfd8503045adce8e2197bbf9697)
* Normalize file-server redirect Location to authority-free single-rooted path
The static file server's canonical 301 redirects (index-file strip, directory trailing-slash add, and file trailing-slash strip in _servefile_response) built the Location header verbatim from the un-normalized request target. Request-target validation only requires a leading '/', has no CTL bytes, and the '..' check plus segment decoding (keepempty=false) drop empty segments, so a target like //evil.example/index.html or /\evil.example/ survived validation and produced a Location such as //evil.example/ -- a scheme-relative network-path reference (RFC 3986 4.2) that browsers resolve to a foreign authority, i.e. an open redirect.
Add _sanitize_redirect_location, which collapses any leading run of '/' or '\\' separators down to a single '/', re-rooting the Location at the server's own origin so it can never carry an authority component. Backslashes are treated as separators because browsers normalize '\\' to '/' in URLs, closing the /\\evil.example bypass. _redirect_response routes every Location through this sanitizer before setting the header, covering all three canonical-redirect branches and any future callers uniformly. Single-rooted local paths (/docs/, /hello.txt, /) are left untouched, preserving existing behavior and backward compatibility.
Add regression coverage exercising the sanitizer directly and end-to-end through servefile/fileserver for the index-strip, directory trailing-slash-add, and file trailing-slash-strip branches; the assertions fail on the un-normalized code.
Reported to the JuliaLang security team through Anthropic's Coordinated
Vulnerability Disclosure program and tracked as JLSEC-2026-617
(Anthropic references: ANT-2026-59AETZK3, ANT-2026-APXT86GW).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 444956f05ad3a7724a5cc05af97eeff27b1a17a4)
* Harden HTTP/1 request parsing against framing-differential smuggling
The HTTP/1 server request parser had three framing primitives that could make HTTP.jl disagree with a fronting proxy about message boundaries on a reused keep-alive connection.
1. Bare-LF line termination. _readline_crlf(::_ConnReader) tolerated a bare LF on its buffered fast path but required CRLF on the slow path, so the accepted header grammar depended on TCP segmentation; an absorbed bare LF later became a space and silently merged headers. Both paths now require a strict CRLF terminator and reject any LF not preceded by CR with a ParseError, so framing is independent of buffer-fill state.
2. Lenient chunk-size. _parse_chunk_size delegated to Base.parse(Int64, ...; base=16), which tolerates a leading sign, a 0x prefix, and isspace padding (including a trailing bare CR). It is replaced with a strict byte-by-byte 1*HEXDIG parser (chunk extension split off at the first ';') that rejects signs, prefixes, surrounding/embedded whitespace, and empty input, with the Int64-range overflow guard preserved.
3. HTTP/1.0 Transfer-Encoding and TE+Content-Length. For proto < HTTP/1.1, _parse_transfer_encoding! previously stripped Transfer-Encoding and fell back to Content-Length while the connection could stay open. It now rejects such messages with a ProtocolError. read_request additionally rejects any request carrying both Transfer-Encoding and Content-Length instead of silently preferring chunked.
All rejections surface as a 400 with the connection force-closed, so no ambiguous trailing bytes remain on the transport. The response-reading and write paths are unchanged except that constructing an HTTP/1.0 message with a Transfer-Encoding header now raises rather than silently dropping the header. Tests cover bare-LF rejection on both the fast and slow _ConnReader paths (including a legitimate CRLF spanning buffer fills), strict chunk-size validation, and the HTTP/1.0-TE and TE+Content-Length rejections.
Reported to the JuliaLang security team through Anthropic's Coordinated
Vulnerability Disclosure program and tracked as JLSEC-2026-618
(Anthropic references: ANT-2026-CN279YCX, ANT-2026-MG2WTZ8Z,
ANT-2026-SRPX7DN1, ANT-2026-YD5QTQDZ).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 705b9a63c67ba8f1737c5508a681710409886b85)
* Reject CR/LF injection in SSE single-line fields and normalize data line breaks
The server-side SSE serializer in src/http_sse.jl wrote the single-line fields event, id and retry verbatim to the text/event-stream wire with no CR/LF filtering, and split the multi-line data field only on '\n', ignoring a bare '\r' that is also a valid SSE line terminator. The SSEEvent constructor validated nothing. An application echoing attacker-influenced text into id/event/retry (e.g. a Last-Event-ID or correlation id) could embed CR/LF to forge additional SSE fields or a blank-line dispatch boundary, injecting complete events into every connected EventSource client.
Changes:
- Add _reject_sse_line_breaks(name, value) that throws ArgumentError on '\n' or '\r'.
- The SSEEvent(data; event, id, retry) keyword constructor now rejects CR/LF in non-nothing event and id, additionally rejects '\0' in id (browser EventSource parsers drop NUL ids), and rejects negative retry so it can only serialize as digits.
- write(::SSEStream, ::SSEEvent) re-validates event and id with the same helper before serialization as defense-in-depth, since an SSEEvent can also be built via the raw 5-arg positional constructor used by the client parser. retry is an Int and string() can only emit digits (plus a leading '-' the constructor rejects), so it cannot carry a line break.
- Add _split_sse_data_lines using split(value, r"\r\n|\r|\n"; keepempty=true) and use it in place of split(data, '\n') so CR, LF and CRLF in data all normalize to separate data: lines and a bare '\r' can no longer smuggle a wire-level field.
The public keyword API stays ergonomic; only malicious or malformed inputs throw. A regression test asserts the exact wire output of a well-formed event, that CR/LF/NUL/negative inputs are rejected via both the keyword and positional construction paths, and that CR/CRLF/LF in data are normalized to separate data: lines with no raw CR on the wire.
Reported to the JuliaLang security team through Anthropic's Coordinated
Vulnerability Disclosure program and tracked as JLSEC-2026-619
(Anthropic references: ANT-2026-G8DXNHAE, ANT-2026-VATEAP9Z,
ANT-2026-7273TGMW, ANT-2026-C4BNTGKK).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 04c29bc045b2f6ca5e95ce10382734be2cfcd79f)
* Serialize WebSocket reader auto-PONG/CLOSE-echo under sendlock
The WebSocket reader task processed incoming frames by calling ws_on_incoming_data! without holding ws.sendlock. That function is not a pure parser: its auto-PONG and CLOSE-echo paths push! onto the shared ws.codec.outgoing_frames Vector (via ws_send_pong!/ws_send_frame!). Application send/ping/pong/close mutate the same Vector (push!/iterate/empty! through _flush_ws_output_locked! -> ws_get_outgoing_data!) while holding ws.sendlock. Because the reader did not take the lock, it provided no exclusion: a remote peer flooding PING frames against a multithreaded server (julia -t N, N>1) could drive concurrent push!/empty! of a Julia Vector from two OS threads, which is undefined behavior and can corrupt the array metadata or segfault the process.
Add a helper _process_incoming_data!(ws, data) that acquires ws.sendlock and, under it, runs ws_on_incoming_data! (so the auto-PONG/CLOSE-echo push! happens under the lock) followed by _flush_ws_output_locked!, making each decode atomic with its flush. Route all three production decode call sites through it: the reader loop in _ws_read_loop! (the race site) and the two handshake-buffered sites in the client open and server upgrade paths (for consistency/defense-in-depth; both run before the reader task starts). The blocking readbytes! in the reader loop stays outside the lock so concurrent senders cannot deadlock the reader; lock ordering is unchanged (close nests closelock->sendlock and releases both before waiting). A concurrency-invariant comment documents why ws_on_incoming_data! must hold ws.sendlock. Add a concurrency regression test that drives the reader path and the application send path from many tasks at once and asserts no failures and a drained buffer; it discriminates the locked vs unlocked path under julia -t N, N>1.
Reported to the JuliaLang security team through Anthropic's Coordinated
Vulnerability Disclosure program and tracked as JLSEC-2026-620
(Anthropic references: ANT-2026-YCN945B6).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit ea3f7cc410a9dee1f9a701f30cffa9b0656926d1)
* Fix thread-safety and out-of-bounds reads in content-type sniffer
The HTTP content-type sniffer had two memory/concurrency bugs.
Number sniffing in isjson stored the strtod end-pointer out-parameter in a
shared module-global Vector{Ptr{UInt8}}. Concurrent sniff calls on a
multithreaded server raced on that single cell between the ccall and the
subsequent read, producing a non-deterministic consumed length and parse
result that an attacker could use to influence content-type classification.
The bytes handed to jl_strtod_c were also not NUL-terminated, so strtod could
read past the provided buffer. Remove the shared global; use a per-call local
Ref for the end pointer; and copy the remaining numeric input into a freshly
allocated NUL-terminated buffer (new helper _sniff_number_buffer) so strtod is
bounded by our own NUL. The consumed-length arithmetic and classification
behavior are unchanged.
The MP4 ftyp matcher iterated for st in 9:4:(boxsize+1) and compared 3-byte
brand windows via _byte_equal(data, st, _SNIFF_MP4) under @inbounds, reading
data[st .. st+2]. The only guard was length(data) < boxsize, so when
boxsize == length(data) the loop read up to data[length(data)+3]: a BoundsError
under forced bounds checks and a silent heap over-read otherwise. Clamp the
loop upper bound to min(boxsize+1, length(data) - length(_SNIFF_MP4) + 1) so
every comparison window lies fully within the buffer.
Add focused tests: concurrent number sniffs that must classify deterministically,
NUL-termination of the numeric buffer, and boundary-length MP4 buffers. The MP4
case is additionally re-run in a subprocess under --check-bounds=yes, which
overrides @inbounds and turns the unpatched over-read into an observable
BoundsError, so the regression deterministically fails on unpatched code
regardless of how the test runner is invoked.
Reported to the JuliaLang security team through Anthropic's Coordinated
Vulnerability Disclosure program and tracked as JLSEC-2026-621
(Anthropic references: ANT-2026-5FMZ73VG, ANT-2026-07KFWYV3,
ANT-2026-9PKP3RJA).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 0b658bc9a9769800d0cf3b223cec9cacef37e670)
* Generate WebSocket masking key and handshake nonce from a CSPRNG
The WebSocket client masking key (ws_send_frame!) and the Sec-WebSocket-Key
handshake nonce (ws_random_handshake_key) were generated with rand(UInt8, n),
which draws from the task-local Xoshiro256++ PRNG. Xoshiro is not
cryptographically secure: its internal state can be recovered from a short run
of observed outputs, and every outbound client frame exposes 4 mask bytes on
the wire. An on-path observer could therefore recover the RNG state and predict
all future masking keys, defeating the RFC 6455 5.3 anti-cache-poisoning
purpose of masking (a predictable mask lets an attacker who also supplies
payload craft wire bytes a non-conformant transparent proxy parses and caches
as HTTP).
Introduce a module-level CSPRNG (const WS_CSPRNG = Random.RandomDevice()) and a
ws_secure_random_bytes helper, and route both the 4-byte masking key and the
16-byte handshake nonce through it. Random was already imported by the module.
Behavior is otherwise unchanged and the wire format and public API are
unaffected.
Add a regression testset that pins the security property: seeding the
task-local RNG identically before two secret generations must not reproduce the
secrets (this fails on the unpatched seedable code), plus shape and
non-constant checks for the 4-byte key and 16-byte nonce. Declare Random in
test/Project.toml so the test loads under the package's explicit test-dependency
convention.
Reported to the JuliaLang security team through Anthropic's Coordinated
Vulnerability Disclosure program and tracked as JLSEC-2026-622
(Anthropic references: ANT-2026-90M07PW7, ANT-2026-ZQ6ARSMP,
ANT-2026-K6WWQH2N).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit d5beeed4125bddcdf6e3f7450f532db4762b1567)
* Strictly validate HTTP/2 request pseudo-headers and Host/:authority consistency
The HTTP/2 server's request validator (_validate_h2_request_headers! in
src/http2_server.jl) only passed :method, :path, and :authority through
_normalize_strict_header_field_value, which rejects CR/LF/CTL but permits
SP/HTAB and applies no host or token grammar. As a result a :method such as
"GET /admin?x=" was accepted, :path could carry interior whitespace, and
:authority was never host-validated; on HTTP/1 downgrade these were written
verbatim into the request line, enabling request smuggling past path-based
ACLs. Separately, a request could carry both a benign :authority and a
mismatched Host header: _decode_h2_request sets request.host from :authority
but leaves the Host header in request.headers, and the HTTP/1 serializer
forwards existing Host headers verbatim, so a proxy could authorize on
:authority while forwarding a hostile Host to an origin.
Bring h2-server pseudo-header validation to the same strictness already used
by the h1 server and h2 client:
- :method must match the RFC 9110 token charset (_valid_header_field_name),
rejecting SP/HTAB/CTL/non-token values.
- :path must not contain interior SP/HTAB (new _string_contains_whitespace_byte
helper in src/http_core.jl; CR/LF/CTL were already rejected).
- :authority must pass _valid_host_header (the h1 server's host validator).
- Per RFC 9113 8.3.1, when :authority is present every Host header value must
equal it; otherwise the request is rejected as malformed. The check iterates
all stored Host entries (not just the first) so multiple/non-adjacent Host
headers cannot smuggle a mismatched authority through HTTP/1 downgrade.
Well-formed and CONNECT request paths are unchanged. Adds regression tests
covering each rejection, the multiple/non-adjacent Host case, and that
well-formed, CONNECT, and matching-Host requests still validate.
Reported to the JuliaLang security team through Anthropic's Coordinated
Vulnerability Disclosure program and tracked as JLSEC-2026-623
(Anthropic references: ANT-2026-565042FN, ANT-2026-CWYA87HX).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit f1d08b61b41fd1024d029036b7784367742a6cd6)
* Decode HPACK header blocks for unknown HTTP/2 streams and enforce CONTINUATION sequencing on the client
The HTTP/2 client's _process_incoming_frame! dropped HEADERS/CONTINUATION frames for stream ids absent from conn.streams via `state === nothing && return nothing`, without passing the header block through conn.decoder. HPACK's dynamic table is connection-scoped and mutated as a side effect of decoding each header block (RFC 7541 2.3.2/4), so skipping even one block permanently desyncs the client decoder from the server encoder, causing later indexed header references on other multiplexed streams to resolve to wrong name/value pairs or throw "HPACK index out of range". This is reachable by a malicious server or by a benign race where trailers arrive after _unregister_stream!. The client also lacked the CONTINUATION sequencing enforcement the server already performs.
Add three read-loop-scoped fields to H2Connection: continuation_stream (id of the stream whose header block is still open, 0 if none), open_header_stream (the live stream a multi-frame block belongs to, so trailing CONTINUATIONs still reach it even if the application thread unregisters the stream mid-block), and orphan_header_block (scratch buffer accumulating header-block fragments for unknown/closed streams). Initialize them in the constructor.
Add _consume_orphan_header_fragment!, which bounds the scratch buffer by max_header_block_bytes, accumulates fragments across HEADERS+CONTINUATION, and on END_HEADERS calls decode_header_block purely for the dynamic-table side effect before discarding, clearing the buffer and continuation_stream in a finally. In _process_incoming_frame!, add a top-of-function CONTINUATION sequencing guard (RFC 7540 6.10): when continuation_stream != 0 the next frame must be a CONTINUATION for that same stream, otherwise a ProtocolError; a CONTINUATION with no open block is also a ProtocolError. Route HEADERS/CONTINUATION for unknown streams through the orphan path and live multi-frame blocks through the captured open_header_stream. All new fields are touched only from the single-threaded read loop, so no extra locking is required.
Add regression tests driving the internal frame handlers over a bare H2Connection: one verifies that a header block for an unregistered stream is still HPACK-decoded so a later stream's indexed references resolve correctly, and one verifies CONTINUATION sequencing is enforced (DATA in place of CONTINUATION, and an unsolicited CONTINUATION, are connection errors; a multi-frame orphan block is decoded across frames).
Reported to the JuliaLang security team through Anthropic's Coordinated
Vulnerability Disclosure program and tracked as JLSEC-2026-624
(Anthropic references: ANT-2026-SCEWC4G3).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit a77a1a2bef3205657c4f0817214a80e32fec4b51)
* Bound high-level WebSocket inbound message buffering
Wire the public maxframesize setting into the underlying WebSocket codec so oversized frame headers are rejected before payload bytes are buffered. High-level WebSocket clients and servers now default to a finite 16 MiB frame/message limit instead of typemax(Int), while callers can still opt into larger limits explicitly. Oversized codec rejections are mapped to WebSocket close code 1009 for parity with the existing post-decode size check.
Adds regression coverage proving a high-level WebSocket rejects an oversized frame at header decode time and that the default maxframesize is finite.
Reported to the JuliaLang security team through Anthropic's Coordinated Vulnerability Disclosure program.
Anthropic references: ANT-2026-119PTS6Z, ANT-2026-69ZRFT60.
* Reject control bytes in HTTP response reason phrases
Validate the HTTP/1 response reason phrase before serializing the status line. Custom reasons containing CR, LF, NUL, DEL, or other control bytes now raise ProtocolError instead of being written verbatim into the wire response, while default and safe custom reason phrases remain unchanged.
Adds regression coverage for both status-line serialization paths and write_response!.
Reported to the JuliaLang security team through Anthropic's Coordinated Vulnerability Disclosure program.
Anthropic references: ANT-2026-47BXJSCB, ANT-2026-B9E07QMW, ANT-2026-DC12T2PB, ANT-2026-HRC4SY7X.
* Reject public-suffix cookies and invalid cookie names
Switch cookie-name validation to the RFC token grammar so delimiters, whitespace, and control bytes cannot be serialized into Cookie or Set-Cookie headers. This closes the any-vs-all validation bug while preserving the existing BoundsError regression for high ASCII cookie names that are valid tokens.
Vendor a generated ASCII subset of the official Public Suffix List (version 2026-06-13_21-47-18_UTC, commit 9186eeeda85cef35b1551d00731464939c765cab) and use standard longest-rule matching, including wildcard and exception rules, to reject Set-Cookie Domain attributes that are public suffixes. Normal registrable-domain cookies such as Domain=example.com remain accepted.
Tests cover delimiter-name rejection, PSL examples including co.uk/github.io/herokuapp.com/s3.amazonaws.com, wildcard/exception behavior, and normal sibling-subdomain cookies.
Reported to the JuliaLang security team through Anthropic's Coordinated Vulnerability Disclosure program.
Anthropic references: ANT-2026-76NKG95A, ANT-2026-DWY11CF6, ANT-2026-RJZMA1RW, ANT-2026-TMN5189D, ANT-2026-TTD0NRAZ, ANT-2026-WF563CJR.
* Ignore HTTP/2 WINDOW_UPDATE frames for unknown client streams
The HTTP/2 client no longer creates stream_send_window entries when a peer sends a stream-level WINDOW_UPDATE for an idle, closed, or otherwise unknown stream. Connection-level updates and tracked live-stream updates are unchanged.
Adds a bare-connection regression test that proves unknown and closed-stream updates do not grow the map while live-stream updates still increase the send window.
Reported to the JuliaLang security team through Anthropic's Coordinated Vulnerability Disclosure program.
Anthropic references: ANT-2026-68EXRPGQ, ANT-2026-8JZYNDQD.
* Clamp peer-advertised HPACK encoder table sizes
Clamp SETTINGS_HEADER_TABLE_SIZE before applying it to the local HPACK encoder on both HTTP/2 server and client connections. Peers may advertise a larger receive table, but HTTP.jl now caps the encoder dynamic table at 64 KiB per connection instead of allowing unbounded peer-controlled growth.
Adds focused tests for the client and server settings handlers and preserves existing zero-size table behavior.
Reported to the JuliaLang security team through Anthropic's Coordinated Vulnerability Disclosure program.
Anthropic reference: ANT-2026-JKEVPKR9.
* Require router regex constraints to match full segments
Route variable regex constraints previously used substring matching, so a route such as /users/{id:[0-9]+} accepted segments like 123abc or abc123 while exposing the full unvalidated segment to handlers.
Require constrained variables to match the entire path segment and add regression tests for prefix/suffix garbage around constrained route parameters.
Covers ANT-2026-A297EQFH.
* Close servefile sources on conditional empty responses
servefile opened the file before evaluating conditional and range headers, but only transferred ownership to the response when servecontent returned a streaming body. Conditional 304, failed-precondition 412, and unsatisfiable-range 416 responses returned EmptyBody and left the opened file for GC-driven finalization.
Finalize every opened servefile source deterministically: streaming responses adopt the IO through _SeekableResponseBody, empty responses close it immediately, and exception paths close before rethrowing. Add focused ownership regression coverage and an If-None-Match servefile check.
Covers ANT-2026-BXCQ4CZK.
* Bound ordinary server request body buffering
The ordinary serve! path materialized request bodies before dispatching handlers. H1 chunked/unknown bodies and H2 request bodies could therefore grow an IOBuffer without any application-visible opportunity to reject the upload first.
Add a default-on max_body_bytes server cap for buffered Request handlers, map cap violations to 413 Content Too Large, and preserve a max_body_bytes=0 escape hatch for legacy unlimited buffering. H2 writes the error response before closing the request read side so oversized streams get a real 413 response instead of reset-only behavior.
Add focused H1 content-length/chunked and H2 raw-frame regressions plus docs for the new limit.
Covers ANT-2026-5FGBYW28 and ANT-2026-K4A2QR9Y.
* Enable a default client decompression output cap
HTTP clients already supported max_decompressed_size, but the default of 0 left automatic gzip/deflate materialization unbounded. A malicious compressed response could therefore inflate until the process ran out of memory unless callers knew to opt in.
Default max_decompressed_size to 64 MiB, keep max_decompressed_size=0 as the explicit unlimited opt-out, and reject negative limits. Extend the decompression-bomb tests to cover both default rejection and the opt-out path.
Covers ANT-2026-4EG5QVV7.
* Bound SSE client line and event buffering
The SSE client parser could accept an initial SSE-looking token, then append an arbitrarily long unterminated line into its partial buffer without invoking user callbacks or enforcing the server-side SSEStream max_len.
Add finite default client parser limits for one response line and one accumulated event, expose max_sse_line_bytes and max_sse_event_bytes on high-level request calls, and preserve explicit zero as the opt-out. Add parser-level and high-level sse_callback regressions for the long-line attack shape.
Covers ANT-2026-PC5S9F2J.
* Escape terminal controls in response display
The text/plain Response display printed server-controlled reason phrases and textual body previews directly to the terminal. ANSI and OSC control sequences in either field could affect a user's REPL terminal when a response was auto-displayed.
Render C0/C1 controls as visible escape text in compact response summaries, response status lines, and textual body previews without changing the underlying response bytes. Add regressions for ESC and BEL in both reason and body output.
Covers ANT-2026-X65C86KB.
* perf(cookies): compact public suffix lookup
Replace the generated Set(String[...]) public suffix table with a Julia port of the compact golang.org/x/net/publicsuffix table walker.
The copied data comes from the latest golang/net origin/master checked during this change (9e7fdbfadb32b0cc7524100014c5cf9b6adc7729), including the Go package's checked-in PSL snapshot and BSD license.
Validation:
- JULIA_NUM_THREADS=4 julia --project=test test/http_cookie_tests.jl
* fix(redirects): require exact host for credentials
Tighten sensitive redirect credential retention to exact normalized host equality after scheme and port checks. This avoids copying caller-supplied Authorization/Cookie headers to child hosts, while leaving domain-scoped cookie policy to the cookie jar.
Update helper coverage for normalized exact hosts and child-host redirects.
* Bump version to v2.4.0
* Fix Server docstring binding
* Relax raw server close probe timeout
* Relax raw HTTP/2 server frame timeout on Windows
* Narrow stream response type for trim compile
* Inline response status line helpers for trim compile
---------
Co-authored-by: Keno Fischer <Keno@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>1 parent 9e830d9 commit b5219f5
38 files changed
Lines changed: 2929 additions & 216 deletions
File tree
- docs/src/guides
- src
- http_public_suffix_data
- test
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
95 | 95 | | |
96 | 96 | | |
97 | 97 | | |
98 | | - | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
99 | 101 | | |
100 | 102 | | |
101 | 103 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
112 | 112 | | |
113 | 113 | | |
114 | 114 | | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
115 | 121 | | |
116 | 122 | | |
117 | 123 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
584 | 584 | | |
585 | 585 | | |
586 | 586 | | |
| 587 | + | |
| 588 | + | |
587 | 589 | | |
588 | 590 | | |
589 | 591 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
248 | 248 | | |
249 | 249 | | |
250 | 250 | | |
251 | | - | |
252 | | - | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
253 | 261 | | |
254 | 262 | | |
255 | 263 | | |
| |||
281 | 289 | | |
282 | 290 | | |
283 | 291 | | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
284 | 313 | | |
285 | 314 | | |
286 | 315 | | |
| |||
331 | 360 | | |
332 | 361 | | |
333 | 362 | | |
334 | | - | |
335 | | - | |
336 | | - | |
337 | | - | |
338 | | - | |
339 | | - | |
340 | | - | |
341 | | - | |
342 | | - | |
343 | | - | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
344 | 392 | | |
345 | | - | |
346 | | - | |
| 393 | + | |
347 | 394 | | |
348 | 395 | | |
349 | 396 | | |
| |||
409 | 456 | | |
410 | 457 | | |
411 | 458 | | |
| 459 | + | |
| 460 | + | |
412 | 461 | | |
413 | 462 | | |
414 | 463 | | |
| |||
479 | 528 | | |
480 | 529 | | |
481 | 530 | | |
482 | | - | |
| 531 | + | |
483 | 532 | | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
484 | 539 | | |
485 | 540 | | |
486 | 541 | | |
487 | 542 | | |
488 | | - | |
489 | | - | |
| 543 | + | |
| 544 | + | |
490 | 545 | | |
491 | 546 | | |
492 | 547 | | |
| |||
698 | 753 | | |
699 | 754 | | |
700 | 755 | | |
| 756 | + | |
| 757 | + | |
701 | 758 | | |
702 | 759 | | |
703 | 760 | | |
| |||
897 | 954 | | |
898 | 955 | | |
899 | 956 | | |
| 957 | + | |
| 958 | + | |
| 959 | + | |
| 960 | + | |
| 961 | + | |
| 962 | + | |
| 963 | + | |
| 964 | + | |
| 965 | + | |
900 | 966 | | |
901 | 967 | | |
902 | 968 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
156 | 156 | | |
157 | 157 | | |
158 | 158 | | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
159 | 178 | | |
160 | 179 | | |
161 | 180 | | |
| |||
343 | 362 | | |
344 | 363 | | |
345 | 364 | | |
346 | | - | |
| 365 | + | |
347 | 366 | | |
348 | 367 | | |
349 | 368 | | |
| |||
752 | 771 | | |
753 | 772 | | |
754 | 773 | | |
| 774 | + | |
| 775 | + | |
| 776 | + | |
| 777 | + | |
| 778 | + | |
| 779 | + | |
| 780 | + | |
| 781 | + | |
| 782 | + | |
| 783 | + | |
| 784 | + | |
| 785 | + | |
| 786 | + | |
| 787 | + | |
| 788 | + | |
| 789 | + | |
| 790 | + | |
| 791 | + | |
| 792 | + | |
| 793 | + | |
| 794 | + | |
| 795 | + | |
| 796 | + | |
| 797 | + | |
| 798 | + | |
| 799 | + | |
| 800 | + | |
| 801 | + | |
| 802 | + | |
| 803 | + | |
| 804 | + | |
| 805 | + | |
| 806 | + | |
| 807 | + | |
| 808 | + | |
| 809 | + | |
| 810 | + | |
| 811 | + | |
| 812 | + | |
| 813 | + | |
| 814 | + | |
| 815 | + | |
| 816 | + | |
| 817 | + | |
755 | 818 | | |
756 | 819 | | |
757 | 820 | | |
| 821 | + | |
| 822 | + | |
| 823 | + | |
| 824 | + | |
| 825 | + | |
| 826 | + | |
| 827 | + | |
| 828 | + | |
| 829 | + | |
| 830 | + | |
| 831 | + | |
| 832 | + | |
758 | 833 | | |
759 | 834 | | |
760 | 835 | | |
| |||
805 | 880 | | |
806 | 881 | | |
807 | 882 | | |
808 | | - | |
809 | | - | |
810 | | - | |
| 883 | + | |
| 884 | + | |
811 | 885 | | |
812 | 886 | | |
813 | 887 | | |
| |||
817 | 891 | | |
818 | 892 | | |
819 | 893 | | |
820 | | - | |
| 894 | + | |
| 895 | + | |
| 896 | + | |
| 897 | + | |
| 898 | + | |
| 899 | + | |
| 900 | + | |
| 901 | + | |
| 902 | + | |
821 | 903 | | |
| 904 | + | |
| 905 | + | |
| 906 | + | |
| 907 | + | |
| 908 | + | |
| 909 | + | |
| 910 | + | |
| 911 | + | |
| 912 | + | |
| 913 | + | |
822 | 914 | | |
823 | 915 | | |
824 | 916 | | |
825 | 917 | | |
826 | 918 | | |
827 | | - | |
828 | | - | |
829 | | - | |
| 919 | + | |
| 920 | + | |
| 921 | + | |
| 922 | + | |
| 923 | + | |
| 924 | + | |
| 925 | + | |
| 926 | + | |
| 927 | + | |
| 928 | + | |
| 929 | + | |
| 930 | + | |
| 931 | + | |
| 932 | + | |
| 933 | + | |
| 934 | + | |
| 935 | + | |
830 | 936 | | |
831 | 937 | | |
832 | 938 | | |
| |||
1014 | 1120 | | |
1015 | 1121 | | |
1016 | 1122 | | |
| 1123 | + | |
| 1124 | + | |
| 1125 | + | |
1017 | 1126 | | |
1018 | 1127 | | |
1019 | 1128 | | |
| |||
0 commit comments