flare's security posture is layered. Each row below lists what that layer enforces — by default, on every request, with no opt-in needed.
| Layer | What it does |
|---|---|
flare.net |
Rejects null bytes, CRLF, @ in IP strings before they reach libc. |
flare.dns |
Blocks injection in hostnames (null / CRLF / @, length limits). |
flare.tls |
TLS 1.2+ only, weak ciphers disabled, SNI always sent. Client and server both supported. The server handshake runs non-blocking on the reactor (TlsConnHandle), so a slow or stalled handshake occupies a connection slot bounded by ServerConfig.idle_timeout_ms rather than a worker. The blocking TlsAcceptor.handshake_fd(fd) path remains for callers driving TLS outside the reactor. |
flare.http |
RFC 7230 token validation on header names and values; CR / LF / \0 rejected at parse time. |
flare.http |
Configurable limits on headers (8 KB), body (10 MB), URI (8 KB). |
flare.http |
Sanitised error responses: 4xx bodies do not echo extractor raise Error(...) messages by default. Logs carry the full message + request id; the client gets a fixed status reason. |
flare.http (sessions) |
Signed session cookies (HMAC-SHA256) expire server-side: CookieSessionStore signs an absolute expiry into the cookie and refuses an expired one or one without; InMemorySessionStore and BackedSessionStore expire the stored entry. Default lifetime one day (DEFAULT_SESSION_TTL_S), ttl_s=0 opts out. |
flare.ws |
Client frames masked per RFC 6455, Sec-WebSocket-Accept verified. |
flare.ws |
CSPRNG nonce for handshake key, UTF-8 validation on TEXT frames (WsConnection.recv: CLOSE 1007 on an invalid final TEXT frame). |
flare.http2 (server) |
RFC 9113 §6.5.2 SETTINGS_MAX_HEADER_LIST_SIZE = 16 KiB advertised and enforced on inbound HEADERS + CONTINUATION accumulation: an oversized header block is closed with RST_STREAM(ENHANCE_YOUR_CALM) (a 1 MiB hard cap applies even when the setting is unset). A CONTINUATION flood trips GOAWAY(ENHANCE_YOUR_CALM) (CVE-2024-27316) -- connection-level because an unterminated header block leaves the shared HPACK context unresynchronizable -- and an inbound RST_STREAM flood trips the same (CVE-2023-44487 rapid reset). SETTINGS_MAX_CONCURRENT_STREAMS = 100 advertised and enforced: a stream past the cap is answered RST_STREAM(REFUSED_STREAM). Frame shape, stream state, pseudo-header well-formedness and flow-control limits are validated per RFC 9113 §4-§8 and reported with GOAWAY carrying the §7 error code. The connection-level receive window is enforced as well as the per-stream one: DATA past it is GOAWAY(FLOW_CONTROL_ERROR), so the 64 MiB buffered-request cap (connection credit is withheld above it) binds a peer that ignores the withheld credit; a request whose content-length is not 1*DIGIT, overflows 63 bits, or appears twice with different values is reset with RST_STREAM(PROTOCOL_ERROR) rather than framed by a wrapped or first-field-only length (RFC 9110 §8.6); a field name with an octet outside visible ASCII, an uppercase letter, or a colon after its first byte is a malformed request (RFC 9113 §8.2.1), so a name like a:b cannot reach an application that re-serialises headers as HTTP/1.1; pixi run conformance runs h2spec against a live server in CI. HPACK stores every decoded header string byte for byte (RFC 7541 §4.1 counts table entries in octets), so a value that is not valid UTF-8 can no longer desynchronise the dynamic table; such a value reaches the handler as the raw octets. A HEADERS frame on a stream id at or below the highest the client has used (including one the server refused) is GOAWAY(PROTOCOL_ERROR), so a refused id cannot be reopened (RFC 9113 §5.1.1). HEADERS on stream 0 is GOAWAY(PROTOCOL_ERROR) (RFC 9113 §6.2) instead of an exception out of the frame handler. A GOAWAY shorter than its 8 mandatory octets is FRAME_SIZE_ERROR (RFC 9113 §6.8, §4.2); it is no longer acted on. The first frame after the client preface must be a SETTINGS frame (RFC 9113 §3.4); anything else is GOAWAY(PROTOCOL_ERROR) and is not answered. max_concurrent_streams = 0 admits no stream (every request is REFUSED_STREAM); it is not treated as unlimited (RFC 9113 §6.5.2). WINDOW_UPDATE, RST_STREAM or DATA on an even stream id (server-initiated, which a server with push disabled never opens) is a PROTOCOL_ERROR connection error because the stream is idle (RFC 9113 §5.1), however high the client's request ids have risen. A self-dependent PRIORITY or a zero-increment WINDOW_UPDATE on an idle stream is a PROTOCOL_ERROR connection error rather than an RST_STREAM (RFC 9113 §5.1: none may be sent on an idle stream), so the id is not remembered as reset and a later request on it is not silently discarded. The HPACK decode budget counts every field of a header block, the last one included, so a block that would expand past the connection's header ceiling is refused (COMPRESSION_ERROR) however it is split into fields (RFC 7541 §4.1 sizes). |
flare.http2 (client) |
Advertises SETTINGS_ENABLE_PUSH = 0 in the preface SETTINGS so servers cannot originate PUSH_PROMISE; if one arrives anyway it is a connection error (GOAWAY(PROTOCOL_ERROR), RFC 9113 §8.4): the frame is not dropped, because its header block could have updated the server's HPACK table and skipping it would desynchronise every later response. RFC 9113 §9.1.1 same-origin enforcement: a request whose URL targets a different (scheme, host, port) than the established connection raises rather than tunneling cross-origin requests over the wrong connection. A response HEADERS on a stream id the client has no stream for (even, or an odd id it never allocated) is GOAWAY(PROTOCOL_ERROR) rather than a response reported ready (RFC 9113 §5.1.1). A second HEADERS on a stream the server has already ended is a STREAM_CLOSED connection error (RFC 9113 §5.1), as it is on the server side. A frame larger than the advertised SETTINGS_MAX_FRAME_SIZE is answered with GOAWAY(FRAME_SIZE_ERROR) (RFC 9113 §4.2) and the rest of the input is discarded, as on the server side; feed no longer raises, so callers that let a raise propagate still send the GOAWAY. The last DATA frame of a response, which closes a stream whose request already ended, draws only the connection-level WINDOW_UPDATE: no frame other than PRIORITY is sent on a closed stream (RFC 9113 §5.1). DATA on a stream the server has reset is a STREAM_CLOSED error (RFC 9113 §5.1), even before any response head arrived; only DATA on a live stream with no head is PROTOCOL_ERROR. |
flare.http (TLS) |
HttpClient over https:// advertises ALPN ["h2", "http/1.1"] and dispatches internally on what the server selected: h2 -> drive HTTP/2 over the TLS stream; http/1.1 (or no ALPN) -> existing HTTP/1.1 wire. The same flare.http.Response is returned either way -- the wire choice never leaks into application code. |
The criticism that drove this:
A request to
/users/<script>alert(1)</script>would echo that string in the 400 body withContent-Type: text/plain. Plain text is safe in a browser, but logs that auto-link or terminals that ANSI-interpret can be surprised. More importantly: parser-error messages are a DoS / log-poisoning surface and should be sanitised or replaced with status-code-only responses.
Default behaviour:
- 400 / 4xx responses use a fixed status reason as the body
(e.g.
"Bad Request","Not Found"). The raised error message is not copied into the response. - The full message (including any user input the extractor was
parsing) is logged to stderr with the request id, so production
debugging works. A line looks like
[flare:bad-request] rid=<id> <message>; the id is the inboundX-Request-Idheader, or-when the request carried none (an id thatRequestIdgenerates itself is not known at the error site). Control bytes in the id and the message are escaped, so a hostile value cannot split or forge a log line. - 500 (handler
raise) is the same: fixed body, full message logged with request id as[flare:handler-error] rid=<id> <message>. This holds on the HTTP/1.1, HTTP/2 and HTTP/3 paths. A handler that raisesHttpStatusErrorchose its message for the client, so that one is sent as is and not logged.
Local-development opt-in:
var srv = HttpServer.bind(SocketAddr.localhost(8080), ServerConfig(
expose_error_messages=True,
))Flipping expose_error_messages to True echoes the raised message
into the response body. Use it locally; do not use it in production.
The fuzz harness fuzz/fuzz_extractors.mojo includes a property that
generates URLs with high-bit / control / HTML-ish payloads and
asserts the response body equals the fixed reason — i.e., that no
fuzzer-generated input ever escapes into a 400 body. The property
runs at every release tag.
63 harnesses today (59 fuzz + 4 property), covering:
- HTTP parsing (request, response, headers, URL, cookies, auth)
- WebSocket frames (mask, opcode, close codes)
- Router paths (linear scan)
- Comptime route trie (oracle: 500K-run check that
ComptimeRouterandRouteragree on every status code for every path) - Reactor / connection state machine (chunk boundaries, churn, shutdown)
- Multicore scheduler shutdown
- Typed extractors
- SIMD header scanner
- QUIC and HTTP/3: varint, long header, packet decrypt, initial handshake, transport parameters, connection id, migration and its amplification limit, retry, early-data length and replay strike, h3 frame, h3 server, h3 response reader, QPACK decode and its dynamic table
- HTTP/2 denial of service: the CONTINUATION flood and the rapid-reset shape of CVE-2023-44487
- Property tests on the timer wheel, headers, auth, WebSocket round-trip
63 harnesses, 9M+ runs combined, zero crashes to date.
All 63 run nightly. Until v0.11 the scheduled job ran ten of
them by name, so the entire QUIC, HTTP/3 and QPACK family and both
HTTP/2 denial-of-service harnesses had never run on a schedule --
only when someone ran fuzz-all by hand. The corpus is cached
between runs, so each night starts from what the last one found
rather than rediscovering it.
Real soak tests as release gates: slow-client, churn, mixed-load. These do not produce a number to brag about. They produce the answer to "can I run this in production?"
Harness, gates, and the three-tier (smoke / extended / 24-hour
release-gate) shape live with the rest of the performance work in
benchmark.md.
Fuzzing samples inputs; the Lean 4 models in
formal/ prove properties for all of them. The
codecs, the HTTP/1.1, HTTP/2, WebSocket, QUIC and HTTP/3 state
machines, and the scheduler are transliterated into Lean and checked
against specs written from the RFCs. Differential tests then run the
models against the Mojo code, so a transliteration error shows up too.
The 0.12 audit (formal/REPORT.md) found 138
issues this way, all fixed. Anchors on every model and a pre-commit
gate keep the proofs about the code that ships.
For security issues, please open a private security advisory on GitHub or email the maintainer directly. Do not file a public issue for vulnerabilities.
flare has zero known production deployments. Treat the maturity gap
honestly: nginx, Go net/http, hyper, and axum all shipped CVEs in
their first two years, and the HTTP/2 HPACK parser is one of the
highest-CVE-risk subsystems in any HTTP server. flare's HPACK is
covered by a dedicated fuzz harness for that reason.