Skip to content

v1.1.3

Choose a tag to compare

@M-Chris M-Chris released this 12 Jul 00:16
· 20 commits to main since this release

@morojs/engine 1.1.3

Security-hardening release. Closes a third-party defensive audit and a
follow-up review of the new defenses. No breaking changes — every new knob
is additive and off/opt-in by default, so this is a drop-in for 1.1.x.

Security hardening

The receive side was already well defended (request smuggling, Content-Length
overflow, slowloris, header/body caps, WS zip-bombs). This release closes the
gaps the audit found — most importantly the response-delivery side.

  • Slow-read response DoS — closed. In-flight responses used to be exempt
    from every timeout, so a client that sent a valid request and then stopped
    reading (or pinned a zero TCP receive window) could hold its queued response
    buffer, kernel socket buffer, and fd indefinitely — the receive-side mirror
    of slowloris. A new responseTimeoutMs budget (default 300 s, the
    response-side twin of requestTimeoutMs) sheds a connection whose outbound
    queue makes no drain progress for the whole budget. Progress is measured by
    the write queue shrinking between sweeps, not by write completion, so a
    genuinely slow-but-steady reader of a large download or SSE stream is never
    cut off — only a stalled/zero-window peer is. An opt-in
    responseBackpressureLimit additionally hard-caps the outbound queue (the
    HTTP mirror of wsBackpressureLimit). The same delivery deadline now also
    bounds a stalled WebSocket consumer and a stalled close-frame flush.
  • Host header discipline (RFC 9112 §3.2). An HTTP/1.1 request with zero or
    more than one Host header is now rejected 400 before any routing sees it —
    absent/duplicate Host is a building block for host-confusion and cache
    poisoning behind a Host-routing proxy. HTTP/1.0 (which predates Host) is
    unaffected.
  • Request-target and header-value byte hygiene. Raw control bytes (C0 other
    than HTAB in values, and DEL) in the request target or any header value are
    now rejected 400 instead of being handed to the app verbatim — closing a
    log-injection / downstream-desync vector. Raw UTF-8 (high bytes) and
    absolute-form targets stay accepted, matching Node. Opt-in maxUriSize
    answers 414 for over-long targets.
  • Sec-WebSocket-Key validation (RFC 6455 §4.1). Malformed keys (not the
    base64 of a 16-byte nonce) are refused at the handshake instead of being fed
    into the accept-key computation.
  • Numeric-limit clamp. Operator-supplied size limits are clamped to a sane
    ceiling (2^48) so no absurd value can overflow the engine's internal size
    arithmetic; the process degrades predictably instead of wrapping.
  • N-API robustness. V8 Set/Get/string-creation paths that previously
    used .Check() (which hard-aborts the Node process on failure under
    allocation pressure) now degrade gracefully.
  • TLS transport. The SSL_select_next_proto ALPN path is documented as not
    reaching the CVE-2024-5535 dangling-pointer case, guarded so a future refactor
    can't regress it.

New options (all additive)

  • ssl.ciphers / ssl.ciphersuites / ssl.ecdhCurve — explicit
    cipher-list, TLS 1.3 ciphersuite, and key-share group policy for
    compliance baselines (PCI/FIPS/hardened profiles). Unset, the host Node's
    OpenSSL defaults apply, exactly as before; invalid values throw from serve()
    rather than booting a lax server.
  • responseTimeoutMs, responseBackpressureLimit, maxUriSize
    the response-side / target-size knobs above (see docs/API.md).
  • probe().capabilities gains responseLimits and tlsPolicy so consumers
    can feature-gate the new options instead of version-sniffing.

MoroJS passes only capability-gated options, so an older framework build simply
doesn't set the new keys — full forward/backward compatibility.

Compatibility

  • No API or behavior changes for existing code; all new surface is
    additive. Drop-in for 1.1.x; MoroJS picks it up via ^1.1.0.
  • Platform/ABI matrix now includes Linux musl arm64 (Alpine on Graviton no
    longer falls back to node:http): macOS arm64/x64, Linux glibc x64/arm64,
    Linux musl x64/arm64, Windows x64 × Node 20–26, prebuilt with npm provenance.

Verification

  • Full socket-level wire matrix green: HTTP conformance + edge + hardening +
    regression, WebSocket, limits, TLS + TLS-hardening, permessage-deflate
    (154 tests), with new regression tests per audit finding — missing/duplicate
    Host, control-byte target/value, maxUriSize → 414, malformed WS key, the
    TLS cipher/group knobs, and the slow-read deadline (a steady slow reader
    receives the full body over both single-respond() and streamed write()
    paths, while a zero-window client is still shed).
  • C++ unit suites under the standalone harness: 141-check HTTP parser +
    WebSocket + permessage-deflate.
  • ASan/UBSan legs and the libFuzzer HTTP/WS/TLS/deflate corpora run in CI;
    the new Host/target/value/WS-key validators were re-fuzzed clean.
  • MoroJS engine integration tests green against this build.

See docs/THREAT_MODEL.md for the full defense inventory and the honest
residuals (e.g. a per-sweep drip peer, bounded by responseBackpressureLimit +
maxConnections).