HTTP and networking

A walk through Cruft's own HTTP and networking crates, the HTTP/1.1 codec, HTTP/2 stack, sockets, WebSocket, fetch, and the URL parser, drawing the line between what works today and the boundaries you should not cross yet: request smuggling, denial of service, credential leaks, and server-side request forgery.

Turning bytes on a socket into a request object is a job that usually belongs to a dedicated parser: llhttp in Node, hyper or httparse in a Rust server, each hardened over years against the specific and dangerous class of bugs that HTTP/1.1 framing invites. Cruft wrote its own. The http-codec crate parses every request its server receives and every response its client reads, and the http2-*, sockets, websocket, fetch-api, and the URL parser sit around it.

You can build on all of it today. A node:http server accepts connections, parses requests, and writes framed responses; fetch follows redirects and restricts schemes; new URL(...) canonicalizes hosts the way a browser does. The happy path is real and its structural parsing is tested. What this page does, section by section, is draw the line between that working surface and the boundaries you should not cross yet, in the terms a network programmer already reasons in: request smuggling, denial of service, credential leaks, and server-side request forgery.

Cruft is alpha (0.0.10). This tier is memory-safe and its everyday paths work, but the HTTP/1.1 codec is lenient about framing in ways that enable request smuggling, and the HTTP/2 stack does not yet enforce its own resource limits. Do not put a Cruft HTTP server, or fetch against untrusted URLs, on an adversarial network. The rest of this page is precise about why.

The HTTP/1.1 codec (http-codec)

http-codec is a small, single-crate parser and serializer for HTTP/1.1 with no external dependencies. Two facts about it matter, and they pull in opposite directions: it is safe in the memory sense, and it is lenient in the framing sense.

Memory safety first, because it is the stronger half. The codec has no unsafe and does not panic on malformed input. It allocates a request body only from bytes actually received, never from the declared Content-Length, so a lying length header cannot drive the process out of memory. Line endings must be CRLF, not bare LF, and header lookup is case-insensitive. The streaming response decoder is well tested for fragmentation: feeding it one byte at a time produces the same result as one whole buffer, which is a property test. A raw request over a socket parses and answers correctly, framing intact:

GET /raw HTTP/1.1\r\nHost: x\r\n\r\n
-> HTTP/1.1 200 OK ... Content-Length: 14 ...

The leniency is the part to respect. HTTP/1.1 framing is where request smuggling lives, and smuggling happens whenever two parsers in a chain (your proxy and your origin) disagree about where one request ends and the next begins. This codec is, today, easier to disagree with than llhttp or nginx:

  • It accepts both Content-Length and Transfer-Encoding on the same message and lets Transfer-Encoding win, rather than rejecting the pair the way a conformant parser must. It keeps only the first of duplicate Content-Length headers instead of rejecting the conflict, and it treats any Transfer-Encoding value merely containing the substring chunked as chunked, so chunked, identity and xchunked slip through.
  • Header names and values are not yet checked for CR, LF, or NUL. Because the encoder writes response headers from raw bytes, a newline inside a header value can inject headers or split the response, which is classic HTTP response splitting.
  • Smaller disagreements each shift where Cruft draws a message boundary: whitespace tolerated between a header name and its colon, a version accepted by prefix so HTTP/9.9 passes, and a body longer than Content-Length silently truncated.
  • There are no size or count limits on the chunked path, and the advertised maxHeaderSize is not enforced, so a stream of tiny chunks accumulates without bound.

None of the smuggling-specific defenses have a conformance or differential test behind them, so treat the memory-safety verdict as inspection-strength, not fuzz-proven. In short, http-codec will not crash and will not blow up memory from a declared length, but its framing is lenient, and framing leniency is exactly what smuggling exploits. Fine for a trusted peer; not yet fit for the open internet.

The HTTP/2 stack (http2-hpack, http2-codec, http2-conn)

HTTP/2 is three crates: http2-hpack for HPACK header compression, http2-codec for frame parsing and serialization, and http2-conn for the server-side connection and stream handling. They are wired into the ALPN path, so when a client negotiates h2 over TLS, these crates parse its bytes. The shape is the same as the HTTP/1.1 codec, only sharper: memory-safe and interoperable on the happy path, and not yet defended against the resource-exhaustion surface that defines HTTP/2.

What holds up: no unsafe and no panic on malformed input across all three crates; the HPACK integer and dynamic-table arithmetic is sound (the varint decode guards against overflow, table eviction accounting is symmetric); and the RFC 7541 HPACK example vectors pass, so the happy path interoperates.

The boundary is that HTTP/2's entire security story is resource limits, and this stack does not yet enforce them. The defensive constants exist in the code (MAX_CONCURRENT_STREAMS, MAX_HEADER_LIST_SIZE, MAX_FRAME_SIZE) and are advertised to peers, but they are not yet wired to enforcing checks. Concretely:

  • HPACK decompression is unbounded. The header decoder pushes into a buffer with no cap on decoded size, and a wire-driven dynamic-table-size update is honored without clamping to the advertised maximum, so a few kilobytes of indexed references to one large entry expand to many megabytes.
  • Stream floods are undefended. RST_STREAM drops a map entry with no reset-rate limit, so the rapid-reset pattern behind CVE-2023-44487 (open a stream, run its HPACK decode, reset, repeat) has nothing stopping it; the concurrent-stream limit is advertised but not enforced; and a CONTINUATION flood accumulates header bytes without bound.
  • Frame-level checks are partial. Inbound frame size is not checked against the advertised maximum, there is no receive-side flow-control accounting, and WINDOW_UPDATE, SETTINGS, and stream-ID rules are only partially validated.

There is no h2spec, HPACK-bomb, or rapid-reset test behind these paths yet. So: the HTTP/2 stack is memory-safe and interoperable and it is live behind the h2 server, but do not expose it to untrusted clients until the resource limits its own constants describe are actually enforced.

The socket layer (sockets)

Under all of the above sits sockets, the layer that talks to the operating system. The name suggests more than the code delivers, so be precise. It is a thin wrapper over Rust's standard std::net sockets, with a background thread per listener polling for new connections on a short sleep loop. There is no raw-syscall async reactor here: no epoll, no kqueue, no libc. That is a reasonable, safe way to build it, and it sets the risk profile.

Because the standard library owns every buffer length and every descriptor close, the memory-safety questions that dominate a low-level socket layer do not arise here. The crate has zero unsafe, zero foreign-function calls, and zero dependencies; double-close is structurally prevented, and a peer reset returns an error rather than panicking. For a networking stack, no unsafe at all is a genuine strength.

The real risks are about resources and API contracts, not memory:

  • No connection ceiling. Nothing caps accepted connections, so a connection flood grows file descriptors and memory without bound. It degrades rather than crashing (the accept loop backs off instead of spinning), but there is no backpressure. Separately, nearly every read and write clones the socket for the duration of the call, which transiently duplicates the descriptor and roughly halves the effective descriptor budget under load.
  • One global lock. Every socket call goes through a single global mutex and unwraps it, panicking on poison. It is latent today, but one future panic while that lock is held would make every subsequent socket call in the process panic.
  • A couple of API-contract bugs. The accept-timeout setter silently ignores its timeout, and the raw-descriptor accessor promises a validity lifetime it cannot enforce, which invites a descriptor-reuse race in an external event loop that uses it.

Error-path testing is thin: the tests are mostly happy-path loopback, and peer reset under pressure, partial writes, descriptor exhaustion, and the UDP surface are largely untested. sockets is a competent, memory-safe, dependency-free standard-socket shim whose production risks are resource bounding, not memory unsafety.

WebSocket (websocket)

The websocket crate is a compact RFC 6455 frame codec plus the handshake-Accept crypto. As a frame parser it is sound; what it is not, yet, is a complete WebSocket server, and the missing pieces are the security-critical ones.

Sound today: no unsafe and no reachable panic on a malformed frame (every malformed case returns an error); the XOR masking transform is correct with no off-by-one; the control-frame rules are enforced on both encode and decode (a control frame stays under 126 bytes and is never fragmented); reserved bits are rejected, correct for a no-extension implementation; and the handshake Accept hash matches the RFC 6455 vector.

Where the server-grade properties are missing:

  • Masking direction is not enforced. The codec is role-agnostic: it reads the mask bit but never requires that a server-received client frame be masked, which RFC 6455 demands. A server built directly on it would silently accept unmasked frames. There is no server or client session type to host this or the other role-dependent rules.
  • No message size bound and no reassembly. The single-frame cap is effectively unlimited, and continuation frames are decoded independently with no message reassembly, so there is no total bound and no fragmentation-bomb guard. The codec does not over-allocate from a lying length itself, but it hands no size policy to whatever buffers a frame.
  • Text UTF-8, close codes, and the inbound handshake are unchecked. Invalid UTF-8 in a Text frame is not caught (the mandatory close 1007 is absent), close codes are unvalidated, and the inbound Sec-WebSocket-Key, Origin, and Version are not validated.

These behaviors are untested because they are unimplemented; the structural parsing, by contrast, is well tested. websocket is a sound, panic-free frame codec that correctly handles the structural rules it implements, but the masking-direction enforcement and the size and reassembly bounds a server needs live in a session layer that does not exist yet.

URL parsing

Cruft's URL parser follows the WHATWG URL Standard, and it is the strongest crate in this tier. Most of this page is caution; this section is mostly commendation.

It is a genuinely spec-faithful WHATWG state machine with real bounds. A port over 65535 is rejected outright, IPv4 32-bit overflow is rejected, IPv6 piece limits hold, and it is unsafe-free and panic-free on malformed input. Most important for security, it has no known host-confusion differential on the http/https surface: the classic SSRF and origin-bypass tricks all resolve the way a browser or the reference url crate resolves them. For example:

new URL("http://0x7f000001/").hostname   // "127.0.0.1"
new URL("http://2130706433/").hostname   // "127.0.0.1"
new URL("http://example.com\\@evil.com/").hostname // "example.com"
new URL("http://exa\tmple.com/").hostname // "example.com"
new URL("http://[::1]/").hostname         // "[::1]"
new URL("http://h:99999/")                // throws: invalid URL

Embedded credentials and the double-@ case, backslash-as-slash, %2f@ confusion, tab and newline stripping, and every IPv4 shorthand (hex, octal, and the decimal loopback encoding) are canonicalized so a downstream check sees the true address. Host parsing runs IDNA before the numeric check, which closes the fullwidth-digit bypass. This is a parser whose job is to not disagree with the next parser in the chain, and it does that job.

The remaining gaps are polish, not exposure: non-special-scheme opaque hosts do not reject the WHATWG forbidden host code points, the blob: origin is simplified, and the test suite, though strong on host edge cases, is a selected subset of the web-platform URL tests rather than the full corpus under a fuzzer.

The fetch client (fetch-api)

There is a capability distinction to get right first. The fetch-api crate implements the WHATWG value types (Headers, Request, Response, Body) and nothing else. There is no fetch() function in it, no socket, no redirect follower, no credential handling. So there is no redirect or SSRF threat model to break here; the value types are unsafe-free and panic-free, and new Request(...) and new Response(...) construct as expected.

One caution on the value types: Headers does not yet reject CR, LF, or NUL in values. Setting a value containing \r\n keeps the \n rather than throwing, and a NUL becomes a space. So the injection class is not closed at the value-type layer; do not rely on Headers to sanitize attacker-influenced header content.

The networked fetch that JavaScript actually calls lives in the host runtime, not in this crate. Two of its properties are done right, and they are the two clients most often get wrong: the redirect follower is capped (20 hops, so no infinite-redirect denial of service), and the scheme is restricted to http and https on both the initial URL and every redirect, so file: targets are rejected. (data: URLs are an exception: they are resolved locally and return their inline payload — fetch("data:text/plain,hello") yields a 200 with body hello.) Redirects and AbortSignal work.

The credential and forgery questions, though, are boundaries you must respect when the URL or the far server is untrusted:

  • A cross-origin redirect does not strip credentials. On a redirect to a different origin, the Authorization header and cookies are forwarded verbatim, and an https-to-http downgrade is not stripped either. A redirect from your API to an attacker host leaks the bearer token.
  • Outgoing headers are not sanitized. Header values are written raw with no CR/LF/NUL check, so a newline in a value can inject request headers, and JavaScript can override Host and Content-Length.
  • No guard against internal addresses. fetch connects to whatever host it is given, including loopback and the cloud metadata endpoint, a server-side request-forgery surface when the URL is untrusted.
  • No response-size cap and no timeout on the body. A hostile server can stream forever or stall the connection.

So the posture is specific: Cruft's fetch caps redirects and restricts schemes correctly, but it leaks credentials across cross-origin redirects, does not sanitize outgoing headers, and has no SSRF or response-size guard. Use it freely against servers you control; do not point it at attacker-influenced URLs yet.