You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@pekko.apache.org by "pjfanning (via GitHub)" <gi...@apache.org> on 2026/09/11 10:48:49 UTC
[PR] Re-pin the threat model to 7f71b13 and absorb what merged since 6740cbd [pekko-http]
pjfanning opened a new pull request, #1295:
URL: https://github.com/apache/pekko-http/pull/1295
### Motivation
Sixteen commits landed on `main` after `THREAT_MODEL.md` was pinned to `6740cbd`, seven of which touch a claim the document makes. One changes a triage line: #1194 documents `idle-timeout` as a bidirectional inactivity timeout that a client sending bytes inside every window keeps alive by design, where §9 called such a connection an "evasion" and therefore in scope.
Checking the error path for #1246 also surfaced a shipped default the model never mentioned: `error-logging-verbosity = full` writes the failing request target into the log at warning level, independent of the client-facing `verbose-error-messages = off` that P3 rests on.
### Modification
Re-pin to `7f71b13`, then:
| Section | Change |
| --- | --- |
| **P5 / §9 slow-loris** | Restated around *inactivity* rather than *evasion*, citing the new `reference.conf` and `timeouts.md` wording. A trickling client is `BY-DESIGN: property-disclaimed` with a *(documented)* citation; the in-scope shape is a connection with **no** traffic that outlives the timeout, or a bound whose cleanup leaks — which is where #1284 sits (one scheduled task per open request surviving a dead connection) |
| **P1 note** | #1281 added as the third discard path beside #1259; #1266 recorded as a bound that was missing outright rather than broken |
| **§9 smuggling** | #1257 beside #1267, with the `5 0` → `0x50` example |
| **§5a** | `max-part-count = 10000` and `error-logging-verbosity = full` |
| **§6** | A multipart input row (there was none) and an `IllegalRequestContext.rawRequestTarget` row |
| **§9 false friends** | `verbose-error-messages = off` governs the client, not the log; what reaches the log is `error-logging-verbosity` |
| **§10.9** | Recommend `error-logging-verbosity = simple` where logs are shipped or alerted on |
| **§11** | Two misuse patterns: building `Raw-Request-URI` from request input (verbatim into the HTTP/1.1 request line with no CR/LF guard; dropped by the HPACK guard on HTTP/2 since #1280), and echoing `rawRequestTarget` unescaped from a custom `ParsingErrorHandler` |
| **§15** | Four back-map rows |
Confidence recounted to 22 documented / 32 maintainer / 0 inferred.
### Result
Every claim is verified against `7f71b13`. The P1 frame-size gap stays stated as open, because #1264 has not merged.
**Draft** because two in-flight PRs will move claims this touches: #1264 closes the P1 frame-size gap, and #1294 escapes control characters in the logged parse-error detail, which narrows the §9 false-friend entry from "log injection and flooding" to flooding alone. Both are one-line follow-ups once they land; I'd rather update this PR than have the model claim a state that is a week stale on merge.
### Tests
Not run - docs only
### References
Refs #1194, #1246, #1257, #1266, #1280, #1281, #1284
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@pekko.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@pekko.apache.org
For additional commands, e-mail: notifications-help@pekko.apache.org
Re: [PR] Re-pin the threat model to b1de40b, absorb what merged, and trim it to what a triager needs [pekko-http]
Posted by "pjfanning (via GitHub)" <gi...@apache.org>.
pjfanning merged PR #1295:
URL: https://github.com/apache/pekko-http/pull/1295
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@pekko.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@pekko.apache.org
For additional commands, e-mail: notifications-help@pekko.apache.org
Re: [PR] Re-pin the threat model to 478c58b, absorb what merged, and trim it to what a triager needs [pekko-http]
Posted by "samueleresca (via GitHub)" <gi...@apache.org>.
samueleresca commented on PR #1295:
URL: https://github.com/apache/pekko-http/pull/1295#issuecomment-6071024374
Taking a look at it this weekend
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@pekko.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@pekko.apache.org
For additional commands, e-mail: notifications-help@pekko.apache.org
Re: [PR] Re-pin the threat model to b1de40b, absorb what merged, and trim it to what a triager needs [pekko-http]
Posted by "samueleresca (via GitHub)" <gi...@apache.org>.
samueleresca commented on code in PR #1295:
URL: https://github.com/apache/pekko-http/pull/1295#discussion_r4237646032
##########
THREAT_MODEL.md:
##########
@@ -47,65 +42,63 @@ Caller roles:
| Benchmarks | `http-bench-jmh` | — | **no** — §3 |
| Lint / build / docs | `http-scalafix`, `docs`, `project`, `scripts`, `legal` | — | **no** — §3 |
-*(maintainer — the in/out split is confirmed by the Pekko maintainers; see §14 Q6)*
+*(maintainer — §14 Q6)*
---
## §3 Out of scope (explicit non-goals)
- **Test kits, benchmarks, scalafix rules, build tooling and documentation sources.** A finding in `http-bench-jmh` or any `*-tests` module is `OUT-OF-MODEL: unsupported-component`. *(maintainer — §14 Q6)*
-- **Pekko HTTP is not a WAF, and not an edge-hardened server.** The documentation says so plainly: applications *"should not be exposed to the public internet directly"* and an *"enterprise grade routing solution"* or a load balancer such as Apache HTTP Server or Nginx *"would be safer"* *(documented — `security.md`)*. See §4.
-- **Pekko HTTP is not an authentication or authorization system.** It ships `authenticateBasic`, `authenticateOAuth2` and `authorize` directives, but these are *plumbing*: the credential check is a function the application supplies. Pekko HTTP has no user store, no session model, and no policy engine. *(maintainer — §14 Q7)*
-- **The actor, stream, remoting and cluster layers** are out of scope here and covered by `apache/pekko`'s threat model.
-- **Attackers who already control the embedding process** are out of scope. *(maintainer — §14 Q7)*
+- **Pekko HTTP is not a WAF, and not an edge-hardened server.** Applications *"should not be exposed to the public internet directly"*; an *"enterprise grade routing solution"* or a load balancer such as Apache HTTP Server or Nginx *"would be safer"* *(documented — `security.md`)*. See §4.
+- **Pekko HTTP is not an authentication or authorization system.** `authenticateBasic`, `authenticateOAuth2` and `authorize` are plumbing: the credential check is a function the application supplies. There is no user store, session model or policy engine. *(maintainer — §14 Q7)*
+- **The actor, stream, remoting and cluster layers** are covered by `apache/pekko`'s threat model.
+- **Attackers who already control the embedding process.** *(maintainer — §14 Q7)*
---
## §4 Trust boundaries and data flow
**The trust boundary is the inbound HTTP request.** Everything derived from wire bytes — request line, headers, cookies, entity, HTTP/2 frames — is attacker-controlled until the application validates it.
-Pekko HTTP's documented posture is unusual and important enough to quote in full:
+Pekko HTTP's documented posture:
> "Pekko HTTP-based applications should not be exposed to the public internet directly. We believe Pekko HTTP behaves pretty well under most known Denial of Service attacks, but if you want the best security, you should use an enterprise grade routing solution. Even using a load balancing solution like an up-to-date version of Apache HTTP Server or Nginx would be safer than exposing Pekko HTTP-based applications directly to the public internet."
> — *(documented — `security.md`, "Security model")*
-Read carefully, this makes a **graded** claim rather than a binary one: Pekko HTTP asserts it *"behaves pretty well under most known"* DoS attacks — not that it is DoS-proof, and not that DoS is out of scope. That hedge was the single most consequential ambiguity in this model for triage; §14 Q1 has now resolved it into the content-vs-volume line, which is the rule triage actually applies.
+This is a graded claim, not a disclaimer of DoS as a class. §14 Q1 resolves it into the content-vs-volume line that triage applies.
### Reachability preconditions per family
-- **Core protocol / HTTP/2** — reachable from raw wire bytes. The strongest in-model surface: a parser defect here needs no application cooperation.
-- **Routing DSL** — reachable from a request that the application's own routes expose. A finding must name the directive and show a route shape a reasonable application would write.
+- **Core protocol / HTTP/2** — reachable from raw wire bytes. A parser defect here needs no application cooperation.
+- **Routing DSL** — reachable from a request the application's own routes expose. A finding must name the directive and show a route shape a reasonable application would write.
- **Marshalling** — reachable from an entity body **only where the application has bound that marshaller to a route**. Findings in the underlying JSON/XML library belong to that library.
-- **CORS** — reachable only where the application has installed the `cors()` directive. It is opt-in, not on by default. See §5a.
+- **CORS** — reachable only where the application has installed `cors()`. Opt-in, not on by default.
- **Caching** — reachable only where the application has installed a caching directive; cache-key correctness is the sharp edge.
---
## §5 Assumptions about the environment
- **Runtime.** A conformant JVM. Pekko HTTP does not defend against a hostile JVM or in-process attacker. *(maintainer — §14 Q7)*
-- **Fronting infrastructure.** The documented expectation is that something sits in front in production *(documented — `security.md`)*. Per §14 Q1 this is load-bearing for *volume* only: the proxy is relied on for flood and slow-loris defence, not for bounding a single request, which is P1's job.
-- **TLS.** Pekko HTTP can terminate TLS itself (`HttpsConnectionContext`) and this is supported, though the documented recommendation to front the service (§4) means termination is commonly the proxy's job in production. Cipher and protocol selection come from the JSSE context the application supplies — Pekko HTTP pins nothing and overrides no JDK default, so cipher strength is a deployment property. *(maintainer — §14 Q8)*
-- **Client IP.** `remote-address-attribute` ships `off` *(documented — `reference.conf`)*. When on, the attribute reflects the **socket** peer, which behind a proxy is the proxy; forwarding headers never feed it. Header-derived client IP is opt-in at the call site: `extractClientIP` reads `X-Forwarded-For` / `X-Real-Ip` and is therefore client-controllable, while `extractDirectClientIP` reads the attribute alone and is not. Choosing between them is the application's decision (§14 Q4). *(documented — `MiscDirectives.scala`)*
+- **Fronting infrastructure.** Something sits in front in production *(documented — `security.md`)*. Per §14 Q1 this is load-bearing for *volume* only: the proxy is relied on for flood and slow-loris defence, not for bounding a single request, which is P1's job.
+- **TLS.** Pekko HTTP can terminate TLS itself (`HttpsConnectionContext`); in a fronted deployment termination is commonly the proxy's job. Cipher and protocol selection come from the JSSE context the application supplies — Pekko HTTP pins nothing and overrides no JDK default. *(maintainer — §14 Q8)*
+- **Client IP.** `remote-address-attribute` ships `off` *(documented — `reference.conf`)*. When on, the attribute is the **socket** peer — behind a proxy, the proxy. `extractClientIP` reads `X-Forwarded-For` / `X-Real-Ip` and is client-controllable; `extractDirectClientIP` reads the attribute alone and is not (§14 Q4). *(documented — `MiscDirectives.scala`)*
Review Comment:
nit
```suggestion
- **Client IP.** `remote-address-attribute` ships `off` *(documented — `reference.conf`)*. When on, the attribute holds the address of whoever opened the TCP connection to Pekko HTTP. With a load balancer or reverse proxy in front, that's the proxy's address.
- `extractClientIP` reads `X-Forwarded-For` / `X-Real-Ip` and is **client-controllable**;
- `extractDirectClientIP` reads the attribute alone and is not (§14 Q4). *(documented —`MiscDirectives.scala`)*
```
##########
THREAT_MODEL.md:
##########
@@ -199,65 +195,63 @@ Pekko HTTP therefore takes the following position *(maintainer)*:
**In scope:**
-- **The remote HTTP client.** Can send arbitrary bytes, malformed framing, oversized or deeply-nested input, many concurrent connections, and abusive HTTP/2 frame sequences. The primary adversary — though per §14 Q1 what this adversary achieves through sheer *volume* is the proxy's problem, not the library's. *(maintainer — §14 Q7)*
+- **The remote HTTP client.** Arbitrary bytes, malformed framing, oversized or deeply-nested input, many concurrent connections, abusive HTTP/2 frame sequences. The primary adversary — though what it achieves through sheer *volume* is the proxy's problem (§14 Q1). *(maintainer — §14 Q7)*
- **A malicious upstream server**, where the application uses the client API against an untrusted endpoint. *(maintainer — §14 Q7)*
- **A cross-origin web attacker**, where the application enables CORS. *(maintainer — §14 Q2)*
**Explicitly out of scope:**
-- **Attackers with code execution in the embedding JVM.** Already inside.
+- **Attackers with code execution in the embedding JVM.**
- **The embedding application itself.** A route that deliberately leaks data is an application bug.
-- **Side-channel observers.** No general timing guarantees are made about routing or parsing. Credential comparison is the exception: `Credentials.Provided.verify` compares in constant time (see §8 P8), so a timing finding there is in scope, while one against an application-supplied `provideVerify` is not. *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala`)*
+- **Side-channel observers.** No general timing guarantees are made about routing or parsing. Credential comparison is the exception (P8): a timing finding against `Credentials.verify` is in scope; one against an application-supplied `provideVerify` is not. *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala`)*
---
## §8 Security properties Pekko HTTP provides
| # | Property & conditions | Violation symptom | Severity | Provenance |
| --- | --- | --- | --- | --- |
-| P1 | **Inbound messages are bounded** by the §5a limits: reaching a limit stops the parse rather than merely reporting it, every header a message carries counts towards the count limits, and buffered bytes are released from the accounting when the buffer is discarded | OOM or unbounded buffering from input *within* documented limits; a limit reached without the parse stopping | **Critical** | *(documented — `reference.conf`, `HttpMessageParser.scala`, `Http2StreamHandling.scala`)* |
-| P2 | **Response splitting is blocked**: illegal response header names and values are `error` by default, and the renderers drop any header whose rendered bytes contain CR, LF or NUL — on the HTTP/1.1 main header block, on chunked-response trailers and chunk extensions, and on the HTTP/2 HPACK path alike | CRLF or NUL in an application-supplied header, trailer or chunk extension reaching the wire | **Critical** | *(documented — `reference.conf`, `Rendering.scala:270`)* |
+| P1 | **Inbound messages are bounded** by the §5a limits: reaching a limit stops the parse, every header a message carries counts towards the count limits, and buffered bytes are released from the accounting when the buffer is discarded | OOM or unbounded buffering from input *within* documented limits; a limit reached without the parse stopping | **Critical** | *(documented — `reference.conf`, `HttpMessageParser.scala`, `Http2FrameParsing.scala`, `Http2StreamHandling.scala`)* |
+| P2 | **Response splitting is blocked**: illegal response header names and values are `error` by default, and the renderers drop any header whose rendered bytes contain CR, LF or NUL — on the HTTP/1.1 header block, chunked-response trailers and the HTTP/2 HPACK path alike — and any chunk extension containing CR or LF | CR or LF in an application-supplied header, trailer or chunk extension, or NUL in a header or trailer, reaching the wire | **Critical** | *(documented — `reference.conf`, `Rendering.isIllegalHeaderChar`, `RenderSupport.renderChunk`)* |
| P3 | **Parse errors do not leak detail to the client** — `verbose-error-messages = off` | Internal parse state or stack detail in a 400 response under defaults | High | *(documented — `reference.conf`)* |
| P4 | **Strict URI and RFC6265 cookie parsing by default**, rather than lenient normalization that invites smuggling | Two components disagreeing on a URI or cookie under `strict` | High | *(documented — `reference.conf`)* |
-| P5 | **Connection and request lifetimes are bounded** — `idle-timeout 60s`, `request-timeout 20s`, `max-connections 1024`, `pipelining-limit 1` | A client holding resources indefinitely under defaults | High | *(documented — `reference.conf`)* |
-| P6 | **HTTP/2 concurrency and stream churn are bounded** — `max-concurrent-streams = 256` caps streams open at once, and `frame-type-throttle` rate-limits the frames that let a peer cycle through that cap: `RST_STREAM` is charged by default at 100 per second with a burst of 100. Concurrency alone is not a bound on *work*, because a reset frees the slot immediately — this is the HTTP/2 Rapid Reset shape (CVE-2023-44487) | Unbounded stream/state growth on one connection, or unbounded stream churn through a bounded concurrency limit | High | *(documented — `reference.conf`)* |
-| P7 | **HTTP/2 header blocks are bounded** — `max-header-list-size = 64 KiB` caps the decompressed header list *and* the accumulated HEADERS + CONTINUATION fragments, so a header block the peer never terminates with `END_HEADERS` cannot grow without bound; over-limit blocks get `GOAWAY(ENHANCE_YOUR_CALM)` rather than being buffered | Unbounded buffering from a CONTINUATION flood or an oversized header list | **Critical** | *(documented — `reference.conf`)* |
-| P8 | **Credential comparison is constant-time** where the verifier calls `Credentials.verify` — it compares via `secure_==`, which XOR-accumulates over the full length after a length check, rather than short-circuiting on the first differing byte | Secret recoverable byte-by-byte from response timing against a `verify`-based verifier | High | *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala:37`)* |
-
-**P1-P7 are default-on properties** — a notable contrast with `apache/pekko`, where the strongest controls must be switched on. P8 is the exception: it holds only for a verifier that calls `Credentials.verify`, which is why §10.4 states it as a downstream responsibility. The boundary of the DoS claim, once this model's largest ambiguity, is now fixed by the §14 Q1 content-vs-volume line.
-
-**A note on how P1 came to be stated this precisely.** P1 has the same history as P2, and for the same reason: the draft asserted it from the §5a table, which records what each limit is *supposed* to bound. Three of those bounds did not hold as written, all fixed at the pinned commit. `max-chunk-count` was tested without an `else` branch, so the `StateResult` of `failEntityStream` was discarded and the parser trampolined into the next chunk — reaching the limit emitted an error per remaining chunk in the buffer instead of stopping the parse ([#1220](https://github.com/apache/pekko-http/pull/1220)). `max-header-count` was not incremented on the branch that merges repeated `Connection` headers into one accumulated header, so a message could repeat that header without ever reaching the limit ([#1255](https://github.com/apache/pekko-http/pull/1255)). And on the HTTP/2 side the connection-level buffer accounting only ever went up: `totalBufferedData` was not decremented when a buffere
d stream was discarded on `RST_STREAM` or on downstream cancellation, so a peer that sent data it never let the handler read ratcheted the counter up permanently — once the leak passed half of `incoming-connection-level-buffer-size` the server stopped replenishing the connection window, it drained to zero, and every stream on that connection stalled ([#1259](https://github.com/apache/pekko-http/pull/1259)). P1 as stated above is a claim about what the parser and the frame handler do when a bound is reached, not about what the table says the bound is for. *(maintainer)*
+| P5 | **Connection and request lifetimes are bounded** — `idle-timeout 60s` closes a connection with no traffic in *either* direction for that long; `request-timeout 20s` bounds handling once a request's entity has been fully received (a body still arriving is not covered; §9); `max-connections 1024`; `pipelining-limit 1`. `idle-timeout` is an inactivity timeout, not a receive deadline (§9) | A connection with no traffic outliving `idle-timeout`; a fully received request outliving `request-timeout` with no response; resources held past either bound after the connection is gone | High | *(documented — `reference.conf`, `timeouts.md`)* |
+| P6 | **HTTP/2 concurrency and stream churn are bounded** — `max-concurrent-streams = 256` caps open streams, and `frame-type-throttle` rate-limits `RST_STREAM` (100/s, burst 100) so a peer cannot cycle through that cap for free (Rapid Reset, CVE-2023-44487) | Unbounded stream/state growth on one connection, or unbounded churn through a bounded concurrency limit | High | *(documented — `reference.conf`)* |
+| P7 | **HTTP/2 header blocks are bounded** — `max-header-list-size = 64 KiB` caps the decompressed header list *and* the accumulated HEADERS + CONTINUATION fragments; over-limit blocks get `GOAWAY(ENHANCE_YOUR_CALM)` rather than being buffered | Unbounded buffering from a CONTINUATION flood or an oversized header list | **Critical** | *(documented — `reference.conf`)* |
+| P8 | **Credential comparison is constant-time** where the verifier calls `Credentials.verify` — `secure_==` XOR-accumulates over the full length after a length check | Secret recoverable byte-by-byte from response timing against a `verify`-based verifier | High | *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala`)* |
Review Comment:
small note here from AI scan: " The length leak is documented in the code but absent from P8."
It is referring to: https://github.com/apache/pekko-http/blame/main/http-core/src/main/scala/org/apache/pekko/http/impl/util/EnhancedByteArray.scala#L31-L35
Theoretically, an attacker could infer the length of a stored secret as the length checks early exit. In practice, I'm not sure how this would be feasible. Specially considering a Pekko HTTP service behind a proxy, it's a long shot.
So I'm pointing this out, but I'm also happy to leave as-is.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@pekko.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@pekko.apache.org
For additional commands, e-mail: notifications-help@pekko.apache.org
Re: [PR] Re-pin the threat model to 478c58b, absorb what merged, and trim it to what a triager needs [pekko-http]
Posted by "pjfanning (via GitHub)" <gi...@apache.org>.
pjfanning commented on code in PR #1295:
URL: https://github.com/apache/pekko-http/pull/1295#discussion_r4237128339
##########
THREAT_MODEL.md:
##########
@@ -186,10 +180,12 @@ Pekko HTTP therefore takes the following position *(maintainer)*:
| Any bound route | Headers, incl. `Host`, `Cookie` | **Yes** | Pekko HTTP: count/length limits. App: semantic trust |
| Any bound route | `X-Forwarded-*`, `X-Real-Ip` | **Yes** — trivially spoofable | **App** — surfaced by `extractClientIP`, never validated; use `extractDirectClientIP` for access control or rate limiting (§14 Q4) |
| Any bound route | Entity body (fixed, chunked, streamed) | **Yes** | Pekko HTTP: size/chunk limits. App: content validation |
-| HTTP/2 | Frames, HPACK table, stream IDs | **Yes** | Pekko HTTP: `max-concurrent-streams` |
+| Route with a multipart unmarshaller | Body parts — boundaries, per-part headers, part count | **Yes** | Pekko HTTP: `max-part-count`, header limits per part, each part carrying only its own headers ([#1279](https://github.com/apache/pekko-http/pull/1279)). App: per-part validation |
+| Custom `ParsingErrorHandler` | `IllegalRequestContext.rawRequestTarget` | **Yes** — the bytes that failed to parse | **App** — escape before logging or echoing; documented at the class |
Review Comment:
Agreed — the target is filled in whenever it was read, so the failure can be a later header or the entity (`HttpRequestParser.illegalRequestContext`, `HttpServerBluePrint.contextOf`). Reworded the §6 row in 3c1f52db4 to say it is the client's request-target bytes as received, and that the failure may lie elsewhere.
##########
THREAT_MODEL.md:
##########
@@ -199,65 +195,63 @@ Pekko HTTP therefore takes the following position *(maintainer)*:
**In scope:**
-- **The remote HTTP client.** Can send arbitrary bytes, malformed framing, oversized or deeply-nested input, many concurrent connections, and abusive HTTP/2 frame sequences. The primary adversary — though per §14 Q1 what this adversary achieves through sheer *volume* is the proxy's problem, not the library's. *(maintainer — §14 Q7)*
+- **The remote HTTP client.** Arbitrary bytes, malformed framing, oversized or deeply-nested input, many concurrent connections, abusive HTTP/2 frame sequences. The primary adversary — though what it achieves through sheer *volume* is the proxy's problem (§14 Q1). *(maintainer — §14 Q7)*
- **A malicious upstream server**, where the application uses the client API against an untrusted endpoint. *(maintainer — §14 Q7)*
- **A cross-origin web attacker**, where the application enables CORS. *(maintainer — §14 Q2)*
**Explicitly out of scope:**
-- **Attackers with code execution in the embedding JVM.** Already inside.
+- **Attackers with code execution in the embedding JVM.**
- **The embedding application itself.** A route that deliberately leaks data is an application bug.
-- **Side-channel observers.** No general timing guarantees are made about routing or parsing. Credential comparison is the exception: `Credentials.Provided.verify` compares in constant time (see §8 P8), so a timing finding there is in scope, while one against an application-supplied `provideVerify` is not. *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala`)*
+- **Side-channel observers.** No general timing guarantees are made about routing or parsing. Credential comparison is the exception (P8): a timing finding against `Credentials.verify` is in scope; one against an application-supplied `provideVerify` is not. *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala`)*
---
## §8 Security properties Pekko HTTP provides
| # | Property & conditions | Violation symptom | Severity | Provenance |
| --- | --- | --- | --- | --- |
-| P1 | **Inbound messages are bounded** by the §5a limits: reaching a limit stops the parse rather than merely reporting it, every header a message carries counts towards the count limits, and buffered bytes are released from the accounting when the buffer is discarded | OOM or unbounded buffering from input *within* documented limits; a limit reached without the parse stopping | **Critical** | *(documented — `reference.conf`, `HttpMessageParser.scala`, `Http2StreamHandling.scala`)* |
-| P2 | **Response splitting is blocked**: illegal response header names and values are `error` by default, and the renderers drop any header whose rendered bytes contain CR, LF or NUL — on the HTTP/1.1 main header block, on chunked-response trailers and chunk extensions, and on the HTTP/2 HPACK path alike | CRLF or NUL in an application-supplied header, trailer or chunk extension reaching the wire | **Critical** | *(documented — `reference.conf`, `Rendering.scala:270`)* |
+| P1 | **Inbound messages are bounded** by the §5a limits: reaching a limit stops the parse, every header a message carries counts towards the count limits, and buffered bytes are released from the accounting when the buffer is discarded | OOM or unbounded buffering from input *within* documented limits; a limit reached without the parse stopping | **Critical** | *(documented — `reference.conf`, `HttpMessageParser.scala`, `Http2FrameParsing.scala`, `Http2StreamHandling.scala`)* |
+| P2 | **Response splitting is blocked**: illegal response header names and values are `error` by default, and the renderers drop any header whose rendered bytes contain CR, LF or NUL — on the HTTP/1.1 header block, chunked-response trailers and chunk extensions, and the HTTP/2 HPACK path alike | CRLF or NUL in an application-supplied header, trailer or chunk extension reaching the wire | **Critical** | *(documented — `reference.conf`, `Rendering.isIllegalHeaderChar`)* |
Review Comment:
Confirmed: `RenderSupport.renderChunk` only drops extensions containing CR or LF. Narrowed P2 in 3c1f52db4 so NUL applies to headers and trailers only, and added `RenderSupport.renderChunk` to the citation. Making the renderer use `Rendering.isIllegalHeaderChar` would be a separate code change.
##########
THREAT_MODEL.md:
##########
@@ -199,65 +195,63 @@ Pekko HTTP therefore takes the following position *(maintainer)*:
**In scope:**
-- **The remote HTTP client.** Can send arbitrary bytes, malformed framing, oversized or deeply-nested input, many concurrent connections, and abusive HTTP/2 frame sequences. The primary adversary — though per §14 Q1 what this adversary achieves through sheer *volume* is the proxy's problem, not the library's. *(maintainer — §14 Q7)*
+- **The remote HTTP client.** Arbitrary bytes, malformed framing, oversized or deeply-nested input, many concurrent connections, abusive HTTP/2 frame sequences. The primary adversary — though what it achieves through sheer *volume* is the proxy's problem (§14 Q1). *(maintainer — §14 Q7)*
- **A malicious upstream server**, where the application uses the client API against an untrusted endpoint. *(maintainer — §14 Q7)*
- **A cross-origin web attacker**, where the application enables CORS. *(maintainer — §14 Q2)*
**Explicitly out of scope:**
-- **Attackers with code execution in the embedding JVM.** Already inside.
+- **Attackers with code execution in the embedding JVM.**
- **The embedding application itself.** A route that deliberately leaks data is an application bug.
-- **Side-channel observers.** No general timing guarantees are made about routing or parsing. Credential comparison is the exception: `Credentials.Provided.verify` compares in constant time (see §8 P8), so a timing finding there is in scope, while one against an application-supplied `provideVerify` is not. *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala`)*
+- **Side-channel observers.** No general timing guarantees are made about routing or parsing. Credential comparison is the exception (P8): a timing finding against `Credentials.verify` is in scope; one against an application-supplied `provideVerify` is not. *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala`)*
---
## §8 Security properties Pekko HTTP provides
| # | Property & conditions | Violation symptom | Severity | Provenance |
| --- | --- | --- | --- | --- |
-| P1 | **Inbound messages are bounded** by the §5a limits: reaching a limit stops the parse rather than merely reporting it, every header a message carries counts towards the count limits, and buffered bytes are released from the accounting when the buffer is discarded | OOM or unbounded buffering from input *within* documented limits; a limit reached without the parse stopping | **Critical** | *(documented — `reference.conf`, `HttpMessageParser.scala`, `Http2StreamHandling.scala`)* |
-| P2 | **Response splitting is blocked**: illegal response header names and values are `error` by default, and the renderers drop any header whose rendered bytes contain CR, LF or NUL — on the HTTP/1.1 main header block, on chunked-response trailers and chunk extensions, and on the HTTP/2 HPACK path alike | CRLF or NUL in an application-supplied header, trailer or chunk extension reaching the wire | **Critical** | *(documented — `reference.conf`, `Rendering.scala:270`)* |
+| P1 | **Inbound messages are bounded** by the §5a limits: reaching a limit stops the parse, every header a message carries counts towards the count limits, and buffered bytes are released from the accounting when the buffer is discarded | OOM or unbounded buffering from input *within* documented limits; a limit reached without the parse stopping | **Critical** | *(documented — `reference.conf`, `HttpMessageParser.scala`, `Http2FrameParsing.scala`, `Http2StreamHandling.scala`)* |
+| P2 | **Response splitting is blocked**: illegal response header names and values are `error` by default, and the renderers drop any header whose rendered bytes contain CR, LF or NUL — on the HTTP/1.1 header block, chunked-response trailers and chunk extensions, and the HTTP/2 HPACK path alike | CRLF or NUL in an application-supplied header, trailer or chunk extension reaching the wire | **Critical** | *(documented — `reference.conf`, `Rendering.isIllegalHeaderChar`)* |
| P3 | **Parse errors do not leak detail to the client** — `verbose-error-messages = off` | Internal parse state or stack detail in a 400 response under defaults | High | *(documented — `reference.conf`)* |
| P4 | **Strict URI and RFC6265 cookie parsing by default**, rather than lenient normalization that invites smuggling | Two components disagreeing on a URI or cookie under `strict` | High | *(documented — `reference.conf`)* |
-| P5 | **Connection and request lifetimes are bounded** — `idle-timeout 60s`, `request-timeout 20s`, `max-connections 1024`, `pipelining-limit 1` | A client holding resources indefinitely under defaults | High | *(documented — `reference.conf`)* |
-| P6 | **HTTP/2 concurrency and stream churn are bounded** — `max-concurrent-streams = 256` caps streams open at once, and `frame-type-throttle` rate-limits the frames that let a peer cycle through that cap: `RST_STREAM` is charged by default at 100 per second with a burst of 100. Concurrency alone is not a bound on *work*, because a reset frees the slot immediately — this is the HTTP/2 Rapid Reset shape (CVE-2023-44487) | Unbounded stream/state growth on one connection, or unbounded stream churn through a bounded concurrency limit | High | *(documented — `reference.conf`)* |
-| P7 | **HTTP/2 header blocks are bounded** — `max-header-list-size = 64 KiB` caps the decompressed header list *and* the accumulated HEADERS + CONTINUATION fragments, so a header block the peer never terminates with `END_HEADERS` cannot grow without bound; over-limit blocks get `GOAWAY(ENHANCE_YOUR_CALM)` rather than being buffered | Unbounded buffering from a CONTINUATION flood or an oversized header list | **Critical** | *(documented — `reference.conf`)* |
-| P8 | **Credential comparison is constant-time** where the verifier calls `Credentials.verify` — it compares via `secure_==`, which XOR-accumulates over the full length after a length check, rather than short-circuiting on the first differing byte | Secret recoverable byte-by-byte from response timing against a `verify`-based verifier | High | *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala:37`)* |
+| P5 | **Connection and request lifetimes are bounded** — `idle-timeout 60s` closes a connection with no traffic in *either* direction for that long; `request-timeout 20s` bounds handling once a request has been received; `max-connections 1024`; `pipelining-limit 1`. `idle-timeout` is an inactivity timeout, not a receive deadline (§9) | A connection with no traffic outliving `idle-timeout`; a received request outliving `request-timeout` with no response; resources held past either bound after the connection is gone | High | *(documented — `reference.conf`, `timeouts.md`)* |
Review Comment:
Confirmed: `TimeoutAccessImpl` schedules the timeout only once `requestEnd` completes. P5 now scopes `request-timeout` to a request whose entity has been fully received, points at §9 for bodies still arriving, and the violation symptom says "fully received" (3c1f52db4).
##########
THREAT_MODEL.md:
##########
@@ -199,65 +195,63 @@ Pekko HTTP therefore takes the following position *(maintainer)*:
**In scope:**
-- **The remote HTTP client.** Can send arbitrary bytes, malformed framing, oversized or deeply-nested input, many concurrent connections, and abusive HTTP/2 frame sequences. The primary adversary — though per §14 Q1 what this adversary achieves through sheer *volume* is the proxy's problem, not the library's. *(maintainer — §14 Q7)*
+- **The remote HTTP client.** Arbitrary bytes, malformed framing, oversized or deeply-nested input, many concurrent connections, abusive HTTP/2 frame sequences. The primary adversary — though what it achieves through sheer *volume* is the proxy's problem (§14 Q1). *(maintainer — §14 Q7)*
- **A malicious upstream server**, where the application uses the client API against an untrusted endpoint. *(maintainer — §14 Q7)*
- **A cross-origin web attacker**, where the application enables CORS. *(maintainer — §14 Q2)*
**Explicitly out of scope:**
-- **Attackers with code execution in the embedding JVM.** Already inside.
+- **Attackers with code execution in the embedding JVM.**
- **The embedding application itself.** A route that deliberately leaks data is an application bug.
-- **Side-channel observers.** No general timing guarantees are made about routing or parsing. Credential comparison is the exception: `Credentials.Provided.verify` compares in constant time (see §8 P8), so a timing finding there is in scope, while one against an application-supplied `provideVerify` is not. *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala`)*
+- **Side-channel observers.** No general timing guarantees are made about routing or parsing. Credential comparison is the exception (P8): a timing finding against `Credentials.verify` is in scope; one against an application-supplied `provideVerify` is not. *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala`)*
---
## §8 Security properties Pekko HTTP provides
| # | Property & conditions | Violation symptom | Severity | Provenance |
| --- | --- | --- | --- | --- |
-| P1 | **Inbound messages are bounded** by the §5a limits: reaching a limit stops the parse rather than merely reporting it, every header a message carries counts towards the count limits, and buffered bytes are released from the accounting when the buffer is discarded | OOM or unbounded buffering from input *within* documented limits; a limit reached without the parse stopping | **Critical** | *(documented — `reference.conf`, `HttpMessageParser.scala`, `Http2StreamHandling.scala`)* |
-| P2 | **Response splitting is blocked**: illegal response header names and values are `error` by default, and the renderers drop any header whose rendered bytes contain CR, LF or NUL — on the HTTP/1.1 main header block, on chunked-response trailers and chunk extensions, and on the HTTP/2 HPACK path alike | CRLF or NUL in an application-supplied header, trailer or chunk extension reaching the wire | **Critical** | *(documented — `reference.conf`, `Rendering.scala:270`)* |
+| P1 | **Inbound messages are bounded** by the §5a limits: reaching a limit stops the parse, every header a message carries counts towards the count limits, and buffered bytes are released from the accounting when the buffer is discarded | OOM or unbounded buffering from input *within* documented limits; a limit reached without the parse stopping | **Critical** | *(documented — `reference.conf`, `HttpMessageParser.scala`, `Http2FrameParsing.scala`, `Http2StreamHandling.scala`)* |
+| P2 | **Response splitting is blocked**: illegal response header names and values are `error` by default, and the renderers drop any header whose rendered bytes contain CR, LF or NUL — on the HTTP/1.1 header block, chunked-response trailers and chunk extensions, and the HTTP/2 HPACK path alike | CRLF or NUL in an application-supplied header, trailer or chunk extension reaching the wire | **Critical** | *(documented — `reference.conf`, `Rendering.isIllegalHeaderChar`)* |
| P3 | **Parse errors do not leak detail to the client** — `verbose-error-messages = off` | Internal parse state or stack detail in a 400 response under defaults | High | *(documented — `reference.conf`)* |
| P4 | **Strict URI and RFC6265 cookie parsing by default**, rather than lenient normalization that invites smuggling | Two components disagreeing on a URI or cookie under `strict` | High | *(documented — `reference.conf`)* |
-| P5 | **Connection and request lifetimes are bounded** — `idle-timeout 60s`, `request-timeout 20s`, `max-connections 1024`, `pipelining-limit 1` | A client holding resources indefinitely under defaults | High | *(documented — `reference.conf`)* |
-| P6 | **HTTP/2 concurrency and stream churn are bounded** — `max-concurrent-streams = 256` caps streams open at once, and `frame-type-throttle` rate-limits the frames that let a peer cycle through that cap: `RST_STREAM` is charged by default at 100 per second with a burst of 100. Concurrency alone is not a bound on *work*, because a reset frees the slot immediately — this is the HTTP/2 Rapid Reset shape (CVE-2023-44487) | Unbounded stream/state growth on one connection, or unbounded stream churn through a bounded concurrency limit | High | *(documented — `reference.conf`)* |
-| P7 | **HTTP/2 header blocks are bounded** — `max-header-list-size = 64 KiB` caps the decompressed header list *and* the accumulated HEADERS + CONTINUATION fragments, so a header block the peer never terminates with `END_HEADERS` cannot grow without bound; over-limit blocks get `GOAWAY(ENHANCE_YOUR_CALM)` rather than being buffered | Unbounded buffering from a CONTINUATION flood or an oversized header list | **Critical** | *(documented — `reference.conf`)* |
-| P8 | **Credential comparison is constant-time** where the verifier calls `Credentials.verify` — it compares via `secure_==`, which XOR-accumulates over the full length after a length check, rather than short-circuiting on the first differing byte | Secret recoverable byte-by-byte from response timing against a `verify`-based verifier | High | *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala:37`)* |
+| P5 | **Connection and request lifetimes are bounded** — `idle-timeout 60s` closes a connection with no traffic in *either* direction for that long; `request-timeout 20s` bounds handling once a request has been received; `max-connections 1024`; `pipelining-limit 1`. `idle-timeout` is an inactivity timeout, not a receive deadline (§9) | A connection with no traffic outliving `idle-timeout`; a received request outliving `request-timeout` with no response; resources held past either bound after the connection is gone | High | *(documented — `reference.conf`, `timeouts.md`)* |
+| P6 | **HTTP/2 concurrency and stream churn are bounded** — `max-concurrent-streams = 256` caps open streams, and `frame-type-throttle` rate-limits `RST_STREAM` (100/s, burst 100) so a peer cannot cycle through that cap for free (Rapid Reset, CVE-2023-44487) | Unbounded stream/state growth on one connection, or unbounded churn through a bounded concurrency limit | High | *(documented — `reference.conf`)* |
+| P7 | **HTTP/2 header blocks are bounded** — `max-header-list-size = 64 KiB` caps the decompressed header list *and* the accumulated HEADERS + CONTINUATION fragments; over-limit blocks get `GOAWAY(ENHANCE_YOUR_CALM)` rather than being buffered | Unbounded buffering from a CONTINUATION flood or an oversized header list | **Critical** | *(documented — `reference.conf`)* |
+| P8 | **Credential comparison is constant-time** where the verifier calls `Credentials.verify` — `secure_==` XOR-accumulates over the full length after a length check | Secret recoverable byte-by-byte from response timing against a `verify`-based verifier | High | *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala`)* |
+| P9 | **HTTP/2 streams are isolated from each other's malformed requests**: a request that fails to parse is answered with a `400` on its own stream, and the connection — its HPACK state and every other stream — carries on. A connection a proxy multiplexes for many clients is what this protects | One stream's malformed request failing, stalling or desynchronising the connection for the other streams | High | *(documented — `RequestErrorFlow.scala`)* |
Review Comment:
Confirmed: `RequestParsing.protocolError` throws `Http2ProtocolException` and `Http2Blueprint.errorHandling` answers with GOAWAY. Narrowed P9 in 3c1f52db4 to a method, target, authority or header field that fails to parse into the model, and named the case that closes the connection instead (missing, duplicate, misplaced or unknown pseudo-headers; `Connection`, `Transfer-Encoding`, `TE` other than `trailers`), citing `RequestParsing.scala` and `Http2Blueprint.errorHandling`.
##########
THREAT_MODEL.md:
##########
@@ -199,65 +195,63 @@ Pekko HTTP therefore takes the following position *(maintainer)*:
**In scope:**
-- **The remote HTTP client.** Can send arbitrary bytes, malformed framing, oversized or deeply-nested input, many concurrent connections, and abusive HTTP/2 frame sequences. The primary adversary — though per §14 Q1 what this adversary achieves through sheer *volume* is the proxy's problem, not the library's. *(maintainer — §14 Q7)*
+- **The remote HTTP client.** Arbitrary bytes, malformed framing, oversized or deeply-nested input, many concurrent connections, abusive HTTP/2 frame sequences. The primary adversary — though what it achieves through sheer *volume* is the proxy's problem (§14 Q1). *(maintainer — §14 Q7)*
- **A malicious upstream server**, where the application uses the client API against an untrusted endpoint. *(maintainer — §14 Q7)*
- **A cross-origin web attacker**, where the application enables CORS. *(maintainer — §14 Q2)*
**Explicitly out of scope:**
-- **Attackers with code execution in the embedding JVM.** Already inside.
+- **Attackers with code execution in the embedding JVM.**
- **The embedding application itself.** A route that deliberately leaks data is an application bug.
-- **Side-channel observers.** No general timing guarantees are made about routing or parsing. Credential comparison is the exception: `Credentials.Provided.verify` compares in constant time (see §8 P8), so a timing finding there is in scope, while one against an application-supplied `provideVerify` is not. *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala`)*
+- **Side-channel observers.** No general timing guarantees are made about routing or parsing. Credential comparison is the exception (P8): a timing finding against `Credentials.verify` is in scope; one against an application-supplied `provideVerify` is not. *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala`)*
---
## §8 Security properties Pekko HTTP provides
| # | Property & conditions | Violation symptom | Severity | Provenance |
| --- | --- | --- | --- | --- |
-| P1 | **Inbound messages are bounded** by the §5a limits: reaching a limit stops the parse rather than merely reporting it, every header a message carries counts towards the count limits, and buffered bytes are released from the accounting when the buffer is discarded | OOM or unbounded buffering from input *within* documented limits; a limit reached without the parse stopping | **Critical** | *(documented — `reference.conf`, `HttpMessageParser.scala`, `Http2StreamHandling.scala`)* |
-| P2 | **Response splitting is blocked**: illegal response header names and values are `error` by default, and the renderers drop any header whose rendered bytes contain CR, LF or NUL — on the HTTP/1.1 main header block, on chunked-response trailers and chunk extensions, and on the HTTP/2 HPACK path alike | CRLF or NUL in an application-supplied header, trailer or chunk extension reaching the wire | **Critical** | *(documented — `reference.conf`, `Rendering.scala:270`)* |
+| P1 | **Inbound messages are bounded** by the §5a limits: reaching a limit stops the parse, every header a message carries counts towards the count limits, and buffered bytes are released from the accounting when the buffer is discarded | OOM or unbounded buffering from input *within* documented limits; a limit reached without the parse stopping | **Critical** | *(documented — `reference.conf`, `HttpMessageParser.scala`, `Http2FrameParsing.scala`, `Http2StreamHandling.scala`)* |
+| P2 | **Response splitting is blocked**: illegal response header names and values are `error` by default, and the renderers drop any header whose rendered bytes contain CR, LF or NUL — on the HTTP/1.1 header block, chunked-response trailers and chunk extensions, and the HTTP/2 HPACK path alike | CRLF or NUL in an application-supplied header, trailer or chunk extension reaching the wire | **Critical** | *(documented — `reference.conf`, `Rendering.isIllegalHeaderChar`)* |
| P3 | **Parse errors do not leak detail to the client** — `verbose-error-messages = off` | Internal parse state or stack detail in a 400 response under defaults | High | *(documented — `reference.conf`)* |
| P4 | **Strict URI and RFC6265 cookie parsing by default**, rather than lenient normalization that invites smuggling | Two components disagreeing on a URI or cookie under `strict` | High | *(documented — `reference.conf`)* |
-| P5 | **Connection and request lifetimes are bounded** — `idle-timeout 60s`, `request-timeout 20s`, `max-connections 1024`, `pipelining-limit 1` | A client holding resources indefinitely under defaults | High | *(documented — `reference.conf`)* |
-| P6 | **HTTP/2 concurrency and stream churn are bounded** — `max-concurrent-streams = 256` caps streams open at once, and `frame-type-throttle` rate-limits the frames that let a peer cycle through that cap: `RST_STREAM` is charged by default at 100 per second with a burst of 100. Concurrency alone is not a bound on *work*, because a reset frees the slot immediately — this is the HTTP/2 Rapid Reset shape (CVE-2023-44487) | Unbounded stream/state growth on one connection, or unbounded stream churn through a bounded concurrency limit | High | *(documented — `reference.conf`)* |
-| P7 | **HTTP/2 header blocks are bounded** — `max-header-list-size = 64 KiB` caps the decompressed header list *and* the accumulated HEADERS + CONTINUATION fragments, so a header block the peer never terminates with `END_HEADERS` cannot grow without bound; over-limit blocks get `GOAWAY(ENHANCE_YOUR_CALM)` rather than being buffered | Unbounded buffering from a CONTINUATION flood or an oversized header list | **Critical** | *(documented — `reference.conf`)* |
-| P8 | **Credential comparison is constant-time** where the verifier calls `Credentials.verify` — it compares via `secure_==`, which XOR-accumulates over the full length after a length check, rather than short-circuiting on the first differing byte | Secret recoverable byte-by-byte from response timing against a `verify`-based verifier | High | *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala:37`)* |
+| P5 | **Connection and request lifetimes are bounded** — `idle-timeout 60s` closes a connection with no traffic in *either* direction for that long; `request-timeout 20s` bounds handling once a request has been received; `max-connections 1024`; `pipelining-limit 1`. `idle-timeout` is an inactivity timeout, not a receive deadline (§9) | A connection with no traffic outliving `idle-timeout`; a received request outliving `request-timeout` with no response; resources held past either bound after the connection is gone | High | *(documented — `reference.conf`, `timeouts.md`)* |
+| P6 | **HTTP/2 concurrency and stream churn are bounded** — `max-concurrent-streams = 256` caps open streams, and `frame-type-throttle` rate-limits `RST_STREAM` (100/s, burst 100) so a peer cannot cycle through that cap for free (Rapid Reset, CVE-2023-44487) | Unbounded stream/state growth on one connection, or unbounded churn through a bounded concurrency limit | High | *(documented — `reference.conf`)* |
+| P7 | **HTTP/2 header blocks are bounded** — `max-header-list-size = 64 KiB` caps the decompressed header list *and* the accumulated HEADERS + CONTINUATION fragments; over-limit blocks get `GOAWAY(ENHANCE_YOUR_CALM)` rather than being buffered | Unbounded buffering from a CONTINUATION flood or an oversized header list | **Critical** | *(documented — `reference.conf`)* |
+| P8 | **Credential comparison is constant-time** where the verifier calls `Credentials.verify` — `secure_==` XOR-accumulates over the full length after a length check | Secret recoverable byte-by-byte from response timing against a `verify`-based verifier | High | *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala`)* |
+| P9 | **HTTP/2 streams are isolated from each other's malformed requests**: a request that fails to parse is answered with a `400` on its own stream, and the connection — its HPACK state and every other stream — carries on. A connection a proxy multiplexes for many clients is what this protects | One stream's malformed request failing, stalling or desynchronising the connection for the other streams | High | *(documented — `RequestErrorFlow.scala`)* |
-**P1-P7 are default-on properties** — a notable contrast with `apache/pekko`, where the strongest controls must be switched on. P8 is the exception: it holds only for a verifier that calls `Credentials.verify`, which is why §10.4 states it as a downstream responsibility. The boundary of the DoS claim, once this model's largest ambiguity, is now fixed by the §14 Q1 content-vs-volume line.
+**P1–P7 and P9 are default-on.** P8 holds only for a verifier that calls `Credentials.verify` (§10.4). The DoS boundary is the §14 Q1 content-vs-volume line.
-**A note on how P1 came to be stated this precisely.** P1 has the same history as P2, and for the same reason: the draft asserted it from the §5a table, which records what each limit is *supposed* to bound. Three of those bounds did not hold as written, all fixed at the pinned commit. `max-chunk-count` was tested without an `else` branch, so the `StateResult` of `failEntityStream` was discarded and the parser trampolined into the next chunk — reaching the limit emitted an error per remaining chunk in the buffer instead of stopping the parse ([#1220](https://github.com/apache/pekko-http/pull/1220)). `max-header-count` was not incremented on the branch that merges repeated `Connection` headers into one accumulated header, so a message could repeat that header without ever reaching the limit ([#1255](https://github.com/apache/pekko-http/pull/1255)). And on the HTTP/2 side the connection-level buffer accounting only ever went up: `totalBufferedData` was not decremented when a buffere
d stream was discarded on `RST_STREAM` or on downstream cancellation, so a peer that sent data it never let the handler read ratcheted the counter up permanently — once the leak passed half of `incoming-connection-level-buffer-size` the server stopped replenishing the connection window, it drained to zero, and every stream on that connection stalled ([#1259](https://github.com/apache/pekko-http/pull/1259)). P1 as stated above is a claim about what the parser and the frame handler do when a bound is reached, not about what the table says the bound is for. *(maintainer)*
-
-**One bound is missing at the pinned commit, and P1 is stated with that gap open.** The HTTP/2 frame parser reads a frame's 24-bit length field and then takes that many bytes with no upper limit (`Http2FrameParsing.scala:193-200`), so a peer can make it buffer up to 16 MiB - 1 for a single frame. That happens *before* HPACK decoding and before the entity is assembled, so none of the three HTTP/2 buffer bounds in §5a constrain it, and the cost is multiplied by the number of connections. A `max-frame-size` setting is in flight ([#1264](https://github.com/apache/pekko-http/pull/1264)); until it lands, a report that an oversized frame is buffered is `VALID` against P1 rather than a §12 model gap — the bound is claimed here and does not yet exist. *(maintainer)*
-
-**A note on how P2 came to be stated this precisely.** The draft asserted P2 from the two `reference.conf` processing-mode defaults alone, without walking the path those settings are supposed to govern. Walking it found the guard was not where the claim assumed: it lived in one `~~(HttpHeader)` overload, so chunked-response **trailers** and **chunk extensions** were rendered around it ([#1256](https://github.com/apache/pekko-http/pull/1256)), it tested for CR and LF but not **NUL** ([#1260](https://github.com/apache/pekko-http/pull/1260)), and the **HTTP/2** header path had no equivalent check at all, so a mitigation an application relied on under HTTP/1.1 disappeared on upgrade ([#1258](https://github.com/apache/pekko-http/pull/1258)). All three are fixed at the pinned commit, and the rule now lives in one predicate (`Rendering.isIllegalHeaderChar`) rather than in four copies. P2 as stated above is a claim about the merged enforcement path, not about the two settings. *(maintainer)
*
+**How §8 was verified.** P1, P2 and P9 were first asserted from configuration and design, then checked against the code; each had defects, all fixed at or before the pinned commit. P1: `max-chunk-count` reported the limit without stopping the parse ([#1220](https://github.com/apache/pekko-http/pull/1220)); repeated `Connection` headers escaped `max-header-count` ([#1255](https://github.com/apache/pekko-http/pull/1255)); HTTP/2 connection-level buffer accounting leaked on discarded streams until the connection window drained and every stream stalled ([#1259](https://github.com/apache/pekko-http/pull/1259), [#1281](https://github.com/apache/pekko-http/pull/1281)); nothing bounded multipart part count ([#1266](https://github.com/apache/pekko-http/pull/1266)); the HTTP/2 frame parser buffered a frame of any declared length, up to 16 MiB − 1, before any other bound applied ([#1264](https://github.com/apache/pekko-http/pull/1264)). P2: the CR/LF guard missed chunked trailers and extens
ions ([#1256](https://github.com/apache/pekko-http/pull/1256)), NUL ([#1260](https://github.com/apache/pekko-http/pull/1260)) and the HTTP/2 header path ([#1258](https://github.com/apache/pekko-http/pull/1258)). P9: a header that failed to parse desynchronised the HPACK dynamic table for every later frame ([#1252](https://github.com/apache/pekko-http/pull/1252)), and a field the HTTP/1.1 parser rejected failed the connection instead of the stream ([#1297](https://github.com/apache/pekko-http/pull/1297)). The properties as stated are claims about the merged code, not about the settings. *(maintainer)*
---
## §9 Security properties Pekko HTTP does **not** provide
-- **No claim of complete DoS resistance.** The documented wording is *"behaves pretty well under most known Denial of Service attacks"*, immediately followed by a recommendation to front it with a load balancer or enterprise routing solution *(documented — `security.md`)*. Per §14 Q1 this disclaimer is **scoped to volume**: Pekko HTTP does not claim to withstand floods, but it *does* claim that one in-limits request cannot provoke disproportionate work — that part is P1, and a violation is `VALID`. *(maintainer — §14 Q1)*
-- **No edge hardening.** Rate limiting, IP reputation, request scrubbing, connection-count throttling beyond `max-connections`, slow-loris mitigation beyond `idle-timeout` — none are provided, and none are planned. *(maintainer — §14 Q1)*
-- **No authentication or authorization.** The security directives are plumbing; the credential check is the application's function. *(maintainer — §14 Q7)*
-- **No CSRF protection.** No token issuance or verification is provided. *(maintainer — §14 Q7)*
+- **No claim of complete DoS resistance.** *"Behaves pretty well under most known Denial of Service attacks"* *(documented — `security.md`)* is scoped to **volume** (§14 Q1): Pekko HTTP does not claim to withstand floods, but does claim that one in-limits request cannot provoke disproportionate work — that is P1, and a violation is `VALID`.
+- **No edge hardening.** Rate limiting, IP reputation, request scrubbing, connection throttling beyond `max-connections`, slow-loris mitigation beyond `idle-timeout` — none provided, none planned. *(maintainer — §14 Q1)*
+- **No authentication or authorization.** The security directives are plumbing. *(maintainer — §14 Q7)*
+- **No CSRF protection.** *(maintainer — §14 Q7)*
- **No output encoding / XSS defence.** Pekko HTTP renders what the application marshals. *(maintainer — §14 Q7)*
-- **No trusted client-IP derivation from headers.** `extractClientIP` surfaces `X-Forwarded-For` / `X-Real-Ip` without validating them, and Pekko HTTP has no trusted-proxy chain configuration. `extractDirectClientIP` is the trustworthy accessor, but it yields the last proxy rather than the client. See §5 and §14 Q4.
+- **No trusted client-IP derivation from headers.** `extractClientIP` surfaces `X-Forwarded-For` / `X-Real-Ip` unvalidated; there is no trusted-proxy chain configuration. `extractDirectClientIP` is trustworthy but yields the last proxy, not the client (§14 Q4).
### False friends
-- **The security directives are not a security *system*.** `authenticateBasic` and `authenticateOAuth2` route credentials to an application-supplied verifier; they impose no password policy, no rate limiting and no lockout. They *do* supply a constant-time comparison (§8 P8) — but only to a verifier that calls `Credentials.verify`; one that pattern-matches the secret out and uses `==` gets none of it.
-- **`allowed-origins = "*"` does not mean "no credentials are exposed".** Combined with the shipped `allow-credentials = yes`, it echoes the caller's `Origin` (§5a).
+- **The security directives are not a security *system*.** `authenticateBasic` and `authenticateOAuth2` route credentials to an application-supplied verifier; no password policy, rate limiting or lockout. They supply a constant-time comparison (P8) only to a verifier that calls `Credentials.verify`.
+- **`allowed-origins = "*"` does not mean "no credentials are exposed".** With the shipped `allow-credentials = yes` it echoes the caller's `Origin` (§5a).
- **`remote-address-attribute` is not the client IP behind a proxy.** It is the socket peer.
- **`max-content-length` is not a global memory bound.** It bounds one entity; concurrent connections multiply it.
+- **`verbose-error-messages = off` is not a log-hygiene setting.** It governs what reaches the *client*. What reaches the *server log* is `error-logging-verbosity`, which ships `full`: a request whose target fails to parse has that target written to the log at warning level (`UriParser.fail`, `logParsingError`). Attacker-chosen bytes in the log at an alerting level are a log-injection and log-flooding consideration the operator owns (§10.9). *(documented — `reference.conf`, `ErrorInfo.scala`)*
Review Comment:
Fixed in 3c1f52db4. Both §9 and the §15 row now cite the actual chain: `UriParser.fail` copies the failing input line into the `ErrorInfo` detail, `DefaultParsingErrorHandler` passes it to `logParsingError`, and that logs `formatPretty` at warning under `full`. `ErrorInfo.scala` is no longer cited.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@pekko.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@pekko.apache.org
For additional commands, e-mail: notifications-help@pekko.apache.org
Re: [PR] Re-pin the threat model to 478c58b, absorb what merged, and trim it to what a triager needs [pekko-http]
Posted by "pjfanning (via GitHub)" <gi...@apache.org>.
pjfanning commented on code in PR #1295:
URL: https://github.com/apache/pekko-http/pull/1295#discussion_r4237128806
##########
THREAT_MODEL.md:
##########
@@ -388,21 +372,26 @@ Every other document **links** to `SECURITY.md` for the reporting policy and to
| Existing statement | Source | Lands in |
| --- | --- | --- |
| Applications should not be exposed to the public internet directly | `security.md` | §3, §4, §10.1 |
-| Behaves "pretty well" under most known DoS attacks — scoped to *volume* by the §14 Q1 ruling | `security.md` + maintainer ruling | §4, §9, §11a, §13, §14 Q1 |
+| Behaves "pretty well" under most known DoS attacks — scoped to *volume* by §14 Q1 | `security.md` + maintainer ruling | §4, §9, §11a, §13, §14 Q1 |
| An enterprise-grade routing solution or LB (httpd, Nginx) is safer | `security.md` | §5, §10.1 |
| Report privately per ASF guidelines; subscribe to announce@ | `security.md` | `SECURITY.md`, §1 |
-| Parsing limits (`max-uri-length`, `max-header-*`, `max-chunk-*`, …) | `http-core/reference.conf` | §5a, §8 P1 |
+| Parsing limits (`max-uri-length`, `max-header-*`, `max-chunk-*`, `max-part-count`, …) | `http-core/reference.conf` | §5a, §8 P1 |
+| `idle-timeout` is a bidirectional inactivity timeout, not a request-receive deadline | `http-core/reference.conf`, `timeouts.md` | §8 P5, §9 |
+| `error-logging-verbosity = full` logs the failing request target at warning level | `http-core/reference.conf`, `ErrorInfo.scala` | §5a, §9, §10.9 |
+| `IllegalRequestContext.rawRequestTarget` is unvalidated, attacker-controlled input | `ParsingErrorHandler.scala` | §6, §11 |
| `verbose-error-messages = off` | `http-core/reference.conf` | §5a, §8 P3 |
| Illegal response header name/value processing = `error` | `http-core/reference.conf` | §5a, §8 P2 |
| `uri-parsing-mode = strict`, `cookie-parsing-mode = rfc6265` | `http-core/reference.conf` | §5a, §8 P4 |
| Timeouts and connection caps | `http-core/reference.conf` | §5a, §8 P5 |
| `max-concurrent-streams = 256` | `http-core/reference.conf` | §5a, §8 P6 |
| `max-header-list-size = 64 KiB`, bounding HEADERS + CONTINUATION accumulation | `http-core/reference.conf` | §5a, §8 P7 |
-| `frame-type-throttle` charging `RST_STREAM` by default, against HTTP/2 Rapid Reset (CVE-2023-44487) | `http-core/reference.conf` | §5a, §8 P6 |
-| HTTP/2 incoming buffer bounds (`incoming-connection-level-buffer-size = 10 MB`, `incoming-stream-level-buffer-size = 512kB`) and `outgoing-control-frame-buffer-size = 1024` | `http-core/reference.conf` | §5a, §8 P1 |
-| Decoding limits (`decode-max-bytes-per-chunk = 1m`, `decode-max-size = 8m`), applied by the `decodeRequest*` directives | `http/reference.conf`, `CodingDirectives.scala` | §5a, §9 |
+| `frame-type-throttle` charging `RST_STREAM` by default, against Rapid Reset (CVE-2023-44487) | `http-core/reference.conf` | §5a, §8 P6 |
+| HTTP/2 `max-frame-size`, incoming buffer bounds and `outgoing-control-frame-buffer-size` | `http-core/reference.conf` | §5a, §8 P1 |
+| `Raw-Request-URI` must be visible ASCII and a custom method a token; neither should be built from request input | `http-model.md`, `headers.scala`, `HttpMethod.scala` | §11 |
+| Decoding limits (`decode-max-bytes-per-chunk`, `decode-max-size`), applied by the `decodeRequest*` directives | `http/reference.conf`, `CodingDirectives.scala` | §5a, §9 |
| `Credentials.verify` compares via constant-time `secure_==` | `SecurityDirectives.scala`, `EnhancedByteArray.scala` | §7, §8 P8, §9, §10.4, §14 Q5 |
-| `safeDirectoryChildPath` rejects `..`/separator segments and enforces canonical containment (its string-prefix comparison was found defective in review; fixed by [#1218](https://github.com/apache/pekko-http/pull/1218)) | `FileAndResourceDirectives.scala` | §9, §14 Q3 |
+| A request that fails to parse is answered with a `400` on its own HTTP/2 stream | `RequestErrorFlow.scala` | §8 P9 |
Review Comment:
Fixed together with the P9 comment in 3c1f52db4: the §15 row now covers fields that fail to parse into the model and says that pseudo-header and connection-specific-header violations close the connection with GOAWAY.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@pekko.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@pekko.apache.org
For additional commands, e-mail: notifications-help@pekko.apache.org
Re: [PR] Re-pin the threat model to 478c58b, absorb what merged, and trim it to what a triager needs [pekko-http]
Posted by "pjfanning (via GitHub)" <gi...@apache.org>.
pjfanning commented on PR #1295:
URL: https://github.com/apache/pekko-http/pull/1295#issuecomment-6015856050
If someone has time to review this, it would be useful to keep the security model up to date as we do get a regular stream of reports from security reporters, especially now that AI makes this much easier.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@pekko.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@pekko.apache.org
For additional commands, e-mail: notifications-help@pekko.apache.org
Re: [PR] Re-pin the threat model to 478c58b, absorb what merged, and trim it to what a triager needs [pekko-http]
Posted by "Copilot (via GitHub)" <gi...@apache.org>.
Copilot commented on code in PR #1295:
URL: https://github.com/apache/pekko-http/pull/1295#discussion_r4237098767
##########
THREAT_MODEL.md:
##########
@@ -199,65 +195,63 @@ Pekko HTTP therefore takes the following position *(maintainer)*:
**In scope:**
-- **The remote HTTP client.** Can send arbitrary bytes, malformed framing, oversized or deeply-nested input, many concurrent connections, and abusive HTTP/2 frame sequences. The primary adversary — though per §14 Q1 what this adversary achieves through sheer *volume* is the proxy's problem, not the library's. *(maintainer — §14 Q7)*
+- **The remote HTTP client.** Arbitrary bytes, malformed framing, oversized or deeply-nested input, many concurrent connections, abusive HTTP/2 frame sequences. The primary adversary — though what it achieves through sheer *volume* is the proxy's problem (§14 Q1). *(maintainer — §14 Q7)*
- **A malicious upstream server**, where the application uses the client API against an untrusted endpoint. *(maintainer — §14 Q7)*
- **A cross-origin web attacker**, where the application enables CORS. *(maintainer — §14 Q2)*
**Explicitly out of scope:**
-- **Attackers with code execution in the embedding JVM.** Already inside.
+- **Attackers with code execution in the embedding JVM.**
- **The embedding application itself.** A route that deliberately leaks data is an application bug.
-- **Side-channel observers.** No general timing guarantees are made about routing or parsing. Credential comparison is the exception: `Credentials.Provided.verify` compares in constant time (see §8 P8), so a timing finding there is in scope, while one against an application-supplied `provideVerify` is not. *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala`)*
+- **Side-channel observers.** No general timing guarantees are made about routing or parsing. Credential comparison is the exception (P8): a timing finding against `Credentials.verify` is in scope; one against an application-supplied `provideVerify` is not. *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala`)*
---
## §8 Security properties Pekko HTTP provides
| # | Property & conditions | Violation symptom | Severity | Provenance |
| --- | --- | --- | --- | --- |
-| P1 | **Inbound messages are bounded** by the §5a limits: reaching a limit stops the parse rather than merely reporting it, every header a message carries counts towards the count limits, and buffered bytes are released from the accounting when the buffer is discarded | OOM or unbounded buffering from input *within* documented limits; a limit reached without the parse stopping | **Critical** | *(documented — `reference.conf`, `HttpMessageParser.scala`, `Http2StreamHandling.scala`)* |
-| P2 | **Response splitting is blocked**: illegal response header names and values are `error` by default, and the renderers drop any header whose rendered bytes contain CR, LF or NUL — on the HTTP/1.1 main header block, on chunked-response trailers and chunk extensions, and on the HTTP/2 HPACK path alike | CRLF or NUL in an application-supplied header, trailer or chunk extension reaching the wire | **Critical** | *(documented — `reference.conf`, `Rendering.scala:270`)* |
+| P1 | **Inbound messages are bounded** by the §5a limits: reaching a limit stops the parse, every header a message carries counts towards the count limits, and buffered bytes are released from the accounting when the buffer is discarded | OOM or unbounded buffering from input *within* documented limits; a limit reached without the parse stopping | **Critical** | *(documented — `reference.conf`, `HttpMessageParser.scala`, `Http2FrameParsing.scala`, `Http2StreamHandling.scala`)* |
+| P2 | **Response splitting is blocked**: illegal response header names and values are `error` by default, and the renderers drop any header whose rendered bytes contain CR, LF or NUL — on the HTTP/1.1 header block, chunked-response trailers and chunk extensions, and the HTTP/2 HPACK path alike | CRLF or NUL in an application-supplied header, trailer or chunk extension reaching the wire | **Critical** | *(documented — `reference.conf`, `Rendering.isIllegalHeaderChar`)* |
Review Comment:
This property currently promises that NUL is filtered from chunk extensions, but `RenderSupport.renderChunk` only rejects `\r` and `\n`; a NUL extension is rendered to the wire. Narrow the property and violation symptom so NUL applies only to headers/trailers, unless the renderer is also changed.
##########
THREAT_MODEL.md:
##########
@@ -186,10 +180,12 @@ Pekko HTTP therefore takes the following position *(maintainer)*:
| Any bound route | Headers, incl. `Host`, `Cookie` | **Yes** | Pekko HTTP: count/length limits. App: semantic trust |
| Any bound route | `X-Forwarded-*`, `X-Real-Ip` | **Yes** — trivially spoofable | **App** — surfaced by `extractClientIP`, never validated; use `extractDirectClientIP` for access control or rate limiting (§14 Q4) |
| Any bound route | Entity body (fixed, chunked, streamed) | **Yes** | Pekko HTTP: size/chunk limits. App: content validation |
-| HTTP/2 | Frames, HPACK table, stream IDs | **Yes** | Pekko HTTP: `max-concurrent-streams` |
+| Route with a multipart unmarshaller | Body parts — boundaries, per-part headers, part count | **Yes** | Pekko HTTP: `max-part-count`, header limits per part, each part carrying only its own headers ([#1279](https://github.com/apache/pekko-http/pull/1279)). App: per-part validation |
+| Custom `ParsingErrorHandler` | `IllegalRequestContext.rawRequestTarget` | **Yes** — the bytes that failed to parse | **App** — escape before logging or echoing; documented at the class |
Review Comment:
The request target is not necessarily the bytes that failed to parse: the context can contain a valid target when parsing later fails on a header or protocol element. Keep the attacker-control warning without incorrectly saying this field caused every failure.
##########
THREAT_MODEL.md:
##########
@@ -199,65 +195,63 @@ Pekko HTTP therefore takes the following position *(maintainer)*:
**In scope:**
-- **The remote HTTP client.** Can send arbitrary bytes, malformed framing, oversized or deeply-nested input, many concurrent connections, and abusive HTTP/2 frame sequences. The primary adversary — though per §14 Q1 what this adversary achieves through sheer *volume* is the proxy's problem, not the library's. *(maintainer — §14 Q7)*
+- **The remote HTTP client.** Arbitrary bytes, malformed framing, oversized or deeply-nested input, many concurrent connections, abusive HTTP/2 frame sequences. The primary adversary — though what it achieves through sheer *volume* is the proxy's problem (§14 Q1). *(maintainer — §14 Q7)*
- **A malicious upstream server**, where the application uses the client API against an untrusted endpoint. *(maintainer — §14 Q7)*
- **A cross-origin web attacker**, where the application enables CORS. *(maintainer — §14 Q2)*
**Explicitly out of scope:**
-- **Attackers with code execution in the embedding JVM.** Already inside.
+- **Attackers with code execution in the embedding JVM.**
- **The embedding application itself.** A route that deliberately leaks data is an application bug.
-- **Side-channel observers.** No general timing guarantees are made about routing or parsing. Credential comparison is the exception: `Credentials.Provided.verify` compares in constant time (see §8 P8), so a timing finding there is in scope, while one against an application-supplied `provideVerify` is not. *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala`)*
+- **Side-channel observers.** No general timing guarantees are made about routing or parsing. Credential comparison is the exception (P8): a timing finding against `Credentials.verify` is in scope; one against an application-supplied `provideVerify` is not. *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala`)*
---
## §8 Security properties Pekko HTTP provides
| # | Property & conditions | Violation symptom | Severity | Provenance |
| --- | --- | --- | --- | --- |
-| P1 | **Inbound messages are bounded** by the §5a limits: reaching a limit stops the parse rather than merely reporting it, every header a message carries counts towards the count limits, and buffered bytes are released from the accounting when the buffer is discarded | OOM or unbounded buffering from input *within* documented limits; a limit reached without the parse stopping | **Critical** | *(documented — `reference.conf`, `HttpMessageParser.scala`, `Http2StreamHandling.scala`)* |
-| P2 | **Response splitting is blocked**: illegal response header names and values are `error` by default, and the renderers drop any header whose rendered bytes contain CR, LF or NUL — on the HTTP/1.1 main header block, on chunked-response trailers and chunk extensions, and on the HTTP/2 HPACK path alike | CRLF or NUL in an application-supplied header, trailer or chunk extension reaching the wire | **Critical** | *(documented — `reference.conf`, `Rendering.scala:270`)* |
+| P1 | **Inbound messages are bounded** by the §5a limits: reaching a limit stops the parse, every header a message carries counts towards the count limits, and buffered bytes are released from the accounting when the buffer is discarded | OOM or unbounded buffering from input *within* documented limits; a limit reached without the parse stopping | **Critical** | *(documented — `reference.conf`, `HttpMessageParser.scala`, `Http2FrameParsing.scala`, `Http2StreamHandling.scala`)* |
+| P2 | **Response splitting is blocked**: illegal response header names and values are `error` by default, and the renderers drop any header whose rendered bytes contain CR, LF or NUL — on the HTTP/1.1 header block, chunked-response trailers and chunk extensions, and the HTTP/2 HPACK path alike | CRLF or NUL in an application-supplied header, trailer or chunk extension reaching the wire | **Critical** | *(documented — `reference.conf`, `Rendering.isIllegalHeaderChar`)* |
| P3 | **Parse errors do not leak detail to the client** — `verbose-error-messages = off` | Internal parse state or stack detail in a 400 response under defaults | High | *(documented — `reference.conf`)* |
| P4 | **Strict URI and RFC6265 cookie parsing by default**, rather than lenient normalization that invites smuggling | Two components disagreeing on a URI or cookie under `strict` | High | *(documented — `reference.conf`)* |
-| P5 | **Connection and request lifetimes are bounded** — `idle-timeout 60s`, `request-timeout 20s`, `max-connections 1024`, `pipelining-limit 1` | A client holding resources indefinitely under defaults | High | *(documented — `reference.conf`)* |
-| P6 | **HTTP/2 concurrency and stream churn are bounded** — `max-concurrent-streams = 256` caps streams open at once, and `frame-type-throttle` rate-limits the frames that let a peer cycle through that cap: `RST_STREAM` is charged by default at 100 per second with a burst of 100. Concurrency alone is not a bound on *work*, because a reset frees the slot immediately — this is the HTTP/2 Rapid Reset shape (CVE-2023-44487) | Unbounded stream/state growth on one connection, or unbounded stream churn through a bounded concurrency limit | High | *(documented — `reference.conf`)* |
-| P7 | **HTTP/2 header blocks are bounded** — `max-header-list-size = 64 KiB` caps the decompressed header list *and* the accumulated HEADERS + CONTINUATION fragments, so a header block the peer never terminates with `END_HEADERS` cannot grow without bound; over-limit blocks get `GOAWAY(ENHANCE_YOUR_CALM)` rather than being buffered | Unbounded buffering from a CONTINUATION flood or an oversized header list | **Critical** | *(documented — `reference.conf`)* |
-| P8 | **Credential comparison is constant-time** where the verifier calls `Credentials.verify` — it compares via `secure_==`, which XOR-accumulates over the full length after a length check, rather than short-circuiting on the first differing byte | Secret recoverable byte-by-byte from response timing against a `verify`-based verifier | High | *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala:37`)* |
+| P5 | **Connection and request lifetimes are bounded** — `idle-timeout 60s` closes a connection with no traffic in *either* direction for that long; `request-timeout 20s` bounds handling once a request has been received; `max-connections 1024`; `pipelining-limit 1`. `idle-timeout` is an inactivity timeout, not a receive deadline (§9) | A connection with no traffic outliving `idle-timeout`; a received request outliving `request-timeout` with no response; resources held past either bound after the connection is gone | High | *(documented — `reference.conf`, `timeouts.md`)* |
+| P6 | **HTTP/2 concurrency and stream churn are bounded** — `max-concurrent-streams = 256` caps open streams, and `frame-type-throttle` rate-limits `RST_STREAM` (100/s, burst 100) so a peer cannot cycle through that cap for free (Rapid Reset, CVE-2023-44487) | Unbounded stream/state growth on one connection, or unbounded churn through a bounded concurrency limit | High | *(documented — `reference.conf`)* |
+| P7 | **HTTP/2 header blocks are bounded** — `max-header-list-size = 64 KiB` caps the decompressed header list *and* the accumulated HEADERS + CONTINUATION fragments; over-limit blocks get `GOAWAY(ENHANCE_YOUR_CALM)` rather than being buffered | Unbounded buffering from a CONTINUATION flood or an oversized header list | **Critical** | *(documented — `reference.conf`)* |
+| P8 | **Credential comparison is constant-time** where the verifier calls `Credentials.verify` — `secure_==` XOR-accumulates over the full length after a length check | Secret recoverable byte-by-byte from response timing against a `verify`-based verifier | High | *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala`)* |
+| P9 | **HTTP/2 streams are isolated from each other's malformed requests**: a request that fails to parse is answered with a `400` on its own stream, and the connection — its HPACK state and every other stream — carries on. A connection a proxy multiplexes for many clients is what this protects | One stream's malformed request failing, stalling or desynchronising the connection for the other streams | High | *(documented — `RequestErrorFlow.scala`)* |
Review Comment:
This overstates stream isolation for all malformed requests. Missing, duplicate, out-of-order, or unexpected pseudo-headers call `RequestParsing.protocolError`, which throws `Http2ProtocolException`; `Http2Blueprint.errorHandling` then emits GOAWAY and closes the connection. The 400 isolation path covers model-level request-field parsing failures, so P9 must be narrowed to that class (or the implementation broadened).
##########
THREAT_MODEL.md:
##########
@@ -388,21 +372,26 @@ Every other document **links** to `SECURITY.md` for the reporting policy and to
| Existing statement | Source | Lands in |
| --- | --- | --- |
| Applications should not be exposed to the public internet directly | `security.md` | §3, §4, §10.1 |
-| Behaves "pretty well" under most known DoS attacks — scoped to *volume* by the §14 Q1 ruling | `security.md` + maintainer ruling | §4, §9, §11a, §13, §14 Q1 |
+| Behaves "pretty well" under most known DoS attacks — scoped to *volume* by §14 Q1 | `security.md` + maintainer ruling | §4, §9, §11a, §13, §14 Q1 |
| An enterprise-grade routing solution or LB (httpd, Nginx) is safer | `security.md` | §5, §10.1 |
| Report privately per ASF guidelines; subscribe to announce@ | `security.md` | `SECURITY.md`, §1 |
-| Parsing limits (`max-uri-length`, `max-header-*`, `max-chunk-*`, …) | `http-core/reference.conf` | §5a, §8 P1 |
+| Parsing limits (`max-uri-length`, `max-header-*`, `max-chunk-*`, `max-part-count`, …) | `http-core/reference.conf` | §5a, §8 P1 |
+| `idle-timeout` is a bidirectional inactivity timeout, not a request-receive deadline | `http-core/reference.conf`, `timeouts.md` | §8 P5, §9 |
+| `error-logging-verbosity = full` logs the failing request target at warning level | `http-core/reference.conf`, `ErrorInfo.scala` | §5a, §9, §10.9 |
+| `IllegalRequestContext.rawRequestTarget` is unvalidated, attacker-controlled input | `ParsingErrorHandler.scala` | §6, §11 |
| `verbose-error-messages = off` | `http-core/reference.conf` | §5a, §8 P3 |
| Illegal response header name/value processing = `error` | `http-core/reference.conf` | §5a, §8 P2 |
| `uri-parsing-mode = strict`, `cookie-parsing-mode = rfc6265` | `http-core/reference.conf` | §5a, §8 P4 |
| Timeouts and connection caps | `http-core/reference.conf` | §5a, §8 P5 |
| `max-concurrent-streams = 256` | `http-core/reference.conf` | §5a, §8 P6 |
| `max-header-list-size = 64 KiB`, bounding HEADERS + CONTINUATION accumulation | `http-core/reference.conf` | §5a, §8 P7 |
-| `frame-type-throttle` charging `RST_STREAM` by default, against HTTP/2 Rapid Reset (CVE-2023-44487) | `http-core/reference.conf` | §5a, §8 P6 |
-| HTTP/2 incoming buffer bounds (`incoming-connection-level-buffer-size = 10 MB`, `incoming-stream-level-buffer-size = 512kB`) and `outgoing-control-frame-buffer-size = 1024` | `http-core/reference.conf` | §5a, §8 P1 |
-| Decoding limits (`decode-max-bytes-per-chunk = 1m`, `decode-max-size = 8m`), applied by the `decodeRequest*` directives | `http/reference.conf`, `CodingDirectives.scala` | §5a, §9 |
+| `frame-type-throttle` charging `RST_STREAM` by default, against Rapid Reset (CVE-2023-44487) | `http-core/reference.conf` | §5a, §8 P6 |
+| HTTP/2 `max-frame-size`, incoming buffer bounds and `outgoing-control-frame-buffer-size` | `http-core/reference.conf` | §5a, §8 P1 |
+| `Raw-Request-URI` must be visible ASCII and a custom method a token; neither should be built from request input | `http-model.md`, `headers.scala`, `HttpMethod.scala` | §11 |
+| Decoding limits (`decode-max-bytes-per-chunk`, `decode-max-size`), applied by the `decodeRequest*` directives | `http/reference.conf`, `CodingDirectives.scala` | §5a, §9 |
| `Credentials.verify` compares via constant-time `secure_==` | `SecurityDirectives.scala`, `EnhancedByteArray.scala` | §7, §8 P8, §9, §10.4, §14 Q5 |
-| `safeDirectoryChildPath` rejects `..`/separator segments and enforces canonical containment (its string-prefix comparison was found defective in review; fixed by [#1218](https://github.com/apache/pekko-http/pull/1218)) | `FileAndResourceDirectives.scala` | §9, §14 Q3 |
+| A request that fails to parse is answered with a `400` on its own HTTP/2 stream | `RequestErrorFlow.scala` | §8 P9 |
Review Comment:
This back-map repeats P9's overbroad claim. `RequestErrorFlow` produces the per-stream 400 only for `BadRequest`; HTTP/2 protocol errors such as duplicate or missing pseudo-headers throw `Http2ProtocolException`, resulting in GOAWAY and connection closure. Scope this row to model-level field parsing failures.
##########
THREAT_MODEL.md:
##########
@@ -199,65 +195,63 @@ Pekko HTTP therefore takes the following position *(maintainer)*:
**In scope:**
-- **The remote HTTP client.** Can send arbitrary bytes, malformed framing, oversized or deeply-nested input, many concurrent connections, and abusive HTTP/2 frame sequences. The primary adversary — though per §14 Q1 what this adversary achieves through sheer *volume* is the proxy's problem, not the library's. *(maintainer — §14 Q7)*
+- **The remote HTTP client.** Arbitrary bytes, malformed framing, oversized or deeply-nested input, many concurrent connections, abusive HTTP/2 frame sequences. The primary adversary — though what it achieves through sheer *volume* is the proxy's problem (§14 Q1). *(maintainer — §14 Q7)*
- **A malicious upstream server**, where the application uses the client API against an untrusted endpoint. *(maintainer — §14 Q7)*
- **A cross-origin web attacker**, where the application enables CORS. *(maintainer — §14 Q2)*
**Explicitly out of scope:**
-- **Attackers with code execution in the embedding JVM.** Already inside.
+- **Attackers with code execution in the embedding JVM.**
- **The embedding application itself.** A route that deliberately leaks data is an application bug.
-- **Side-channel observers.** No general timing guarantees are made about routing or parsing. Credential comparison is the exception: `Credentials.Provided.verify` compares in constant time (see §8 P8), so a timing finding there is in scope, while one against an application-supplied `provideVerify` is not. *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala`)*
+- **Side-channel observers.** No general timing guarantees are made about routing or parsing. Credential comparison is the exception (P8): a timing finding against `Credentials.verify` is in scope; one against an application-supplied `provideVerify` is not. *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala`)*
---
## §8 Security properties Pekko HTTP provides
| # | Property & conditions | Violation symptom | Severity | Provenance |
| --- | --- | --- | --- | --- |
-| P1 | **Inbound messages are bounded** by the §5a limits: reaching a limit stops the parse rather than merely reporting it, every header a message carries counts towards the count limits, and buffered bytes are released from the accounting when the buffer is discarded | OOM or unbounded buffering from input *within* documented limits; a limit reached without the parse stopping | **Critical** | *(documented — `reference.conf`, `HttpMessageParser.scala`, `Http2StreamHandling.scala`)* |
-| P2 | **Response splitting is blocked**: illegal response header names and values are `error` by default, and the renderers drop any header whose rendered bytes contain CR, LF or NUL — on the HTTP/1.1 main header block, on chunked-response trailers and chunk extensions, and on the HTTP/2 HPACK path alike | CRLF or NUL in an application-supplied header, trailer or chunk extension reaching the wire | **Critical** | *(documented — `reference.conf`, `Rendering.scala:270`)* |
+| P1 | **Inbound messages are bounded** by the §5a limits: reaching a limit stops the parse, every header a message carries counts towards the count limits, and buffered bytes are released from the accounting when the buffer is discarded | OOM or unbounded buffering from input *within* documented limits; a limit reached without the parse stopping | **Critical** | *(documented — `reference.conf`, `HttpMessageParser.scala`, `Http2FrameParsing.scala`, `Http2StreamHandling.scala`)* |
+| P2 | **Response splitting is blocked**: illegal response header names and values are `error` by default, and the renderers drop any header whose rendered bytes contain CR, LF or NUL — on the HTTP/1.1 header block, chunked-response trailers and chunk extensions, and the HTTP/2 HPACK path alike | CRLF or NUL in an application-supplied header, trailer or chunk extension reaching the wire | **Critical** | *(documented — `reference.conf`, `Rendering.isIllegalHeaderChar`)* |
| P3 | **Parse errors do not leak detail to the client** — `verbose-error-messages = off` | Internal parse state or stack detail in a 400 response under defaults | High | *(documented — `reference.conf`)* |
| P4 | **Strict URI and RFC6265 cookie parsing by default**, rather than lenient normalization that invites smuggling | Two components disagreeing on a URI or cookie under `strict` | High | *(documented — `reference.conf`)* |
-| P5 | **Connection and request lifetimes are bounded** — `idle-timeout 60s`, `request-timeout 20s`, `max-connections 1024`, `pipelining-limit 1` | A client holding resources indefinitely under defaults | High | *(documented — `reference.conf`)* |
-| P6 | **HTTP/2 concurrency and stream churn are bounded** — `max-concurrent-streams = 256` caps streams open at once, and `frame-type-throttle` rate-limits the frames that let a peer cycle through that cap: `RST_STREAM` is charged by default at 100 per second with a burst of 100. Concurrency alone is not a bound on *work*, because a reset frees the slot immediately — this is the HTTP/2 Rapid Reset shape (CVE-2023-44487) | Unbounded stream/state growth on one connection, or unbounded stream churn through a bounded concurrency limit | High | *(documented — `reference.conf`)* |
-| P7 | **HTTP/2 header blocks are bounded** — `max-header-list-size = 64 KiB` caps the decompressed header list *and* the accumulated HEADERS + CONTINUATION fragments, so a header block the peer never terminates with `END_HEADERS` cannot grow without bound; over-limit blocks get `GOAWAY(ENHANCE_YOUR_CALM)` rather than being buffered | Unbounded buffering from a CONTINUATION flood or an oversized header list | **Critical** | *(documented — `reference.conf`)* |
-| P8 | **Credential comparison is constant-time** where the verifier calls `Credentials.verify` — it compares via `secure_==`, which XOR-accumulates over the full length after a length check, rather than short-circuiting on the first differing byte | Secret recoverable byte-by-byte from response timing against a `verify`-based verifier | High | *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala:37`)* |
+| P5 | **Connection and request lifetimes are bounded** — `idle-timeout 60s` closes a connection with no traffic in *either* direction for that long; `request-timeout 20s` bounds handling once a request has been received; `max-connections 1024`; `pipelining-limit 1`. `idle-timeout` is an inactivity timeout, not a receive deadline (§9) | A connection with no traffic outliving `idle-timeout`; a received request outliving `request-timeout` with no response; resources held past either bound after the connection is gone | High | *(documented — `reference.conf`, `timeouts.md`)* |
+| P6 | **HTTP/2 concurrency and stream churn are bounded** — `max-concurrent-streams = 256` caps open streams, and `frame-type-throttle` rate-limits `RST_STREAM` (100/s, burst 100) so a peer cannot cycle through that cap for free (Rapid Reset, CVE-2023-44487) | Unbounded stream/state growth on one connection, or unbounded churn through a bounded concurrency limit | High | *(documented — `reference.conf`)* |
+| P7 | **HTTP/2 header blocks are bounded** — `max-header-list-size = 64 KiB` caps the decompressed header list *and* the accumulated HEADERS + CONTINUATION fragments; over-limit blocks get `GOAWAY(ENHANCE_YOUR_CALM)` rather than being buffered | Unbounded buffering from a CONTINUATION flood or an oversized header list | **Critical** | *(documented — `reference.conf`)* |
+| P8 | **Credential comparison is constant-time** where the verifier calls `Credentials.verify` — `secure_==` XOR-accumulates over the full length after a length check | Secret recoverable byte-by-byte from response timing against a `verify`-based verifier | High | *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala`)* |
+| P9 | **HTTP/2 streams are isolated from each other's malformed requests**: a request that fails to parse is answered with a `400` on its own stream, and the connection — its HPACK state and every other stream — carries on. A connection a proxy multiplexes for many clients is what this protects | One stream's malformed request failing, stalling or desynchronising the connection for the other streams | High | *(documented — `RequestErrorFlow.scala`)* |
-**P1-P7 are default-on properties** — a notable contrast with `apache/pekko`, where the strongest controls must be switched on. P8 is the exception: it holds only for a verifier that calls `Credentials.verify`, which is why §10.4 states it as a downstream responsibility. The boundary of the DoS claim, once this model's largest ambiguity, is now fixed by the §14 Q1 content-vs-volume line.
+**P1–P7 and P9 are default-on.** P8 holds only for a verifier that calls `Credentials.verify` (§10.4). The DoS boundary is the §14 Q1 content-vs-volume line.
-**A note on how P1 came to be stated this precisely.** P1 has the same history as P2, and for the same reason: the draft asserted it from the §5a table, which records what each limit is *supposed* to bound. Three of those bounds did not hold as written, all fixed at the pinned commit. `max-chunk-count` was tested without an `else` branch, so the `StateResult` of `failEntityStream` was discarded and the parser trampolined into the next chunk — reaching the limit emitted an error per remaining chunk in the buffer instead of stopping the parse ([#1220](https://github.com/apache/pekko-http/pull/1220)). `max-header-count` was not incremented on the branch that merges repeated `Connection` headers into one accumulated header, so a message could repeat that header without ever reaching the limit ([#1255](https://github.com/apache/pekko-http/pull/1255)). And on the HTTP/2 side the connection-level buffer accounting only ever went up: `totalBufferedData` was not decremented when a buffere
d stream was discarded on `RST_STREAM` or on downstream cancellation, so a peer that sent data it never let the handler read ratcheted the counter up permanently — once the leak passed half of `incoming-connection-level-buffer-size` the server stopped replenishing the connection window, it drained to zero, and every stream on that connection stalled ([#1259](https://github.com/apache/pekko-http/pull/1259)). P1 as stated above is a claim about what the parser and the frame handler do when a bound is reached, not about what the table says the bound is for. *(maintainer)*
-
-**One bound is missing at the pinned commit, and P1 is stated with that gap open.** The HTTP/2 frame parser reads a frame's 24-bit length field and then takes that many bytes with no upper limit (`Http2FrameParsing.scala:193-200`), so a peer can make it buffer up to 16 MiB - 1 for a single frame. That happens *before* HPACK decoding and before the entity is assembled, so none of the three HTTP/2 buffer bounds in §5a constrain it, and the cost is multiplied by the number of connections. A `max-frame-size` setting is in flight ([#1264](https://github.com/apache/pekko-http/pull/1264)); until it lands, a report that an oversized frame is buffered is `VALID` against P1 rather than a §12 model gap — the bound is claimed here and does not yet exist. *(maintainer)*
-
-**A note on how P2 came to be stated this precisely.** The draft asserted P2 from the two `reference.conf` processing-mode defaults alone, without walking the path those settings are supposed to govern. Walking it found the guard was not where the claim assumed: it lived in one `~~(HttpHeader)` overload, so chunked-response **trailers** and **chunk extensions** were rendered around it ([#1256](https://github.com/apache/pekko-http/pull/1256)), it tested for CR and LF but not **NUL** ([#1260](https://github.com/apache/pekko-http/pull/1260)), and the **HTTP/2** header path had no equivalent check at all, so a mitigation an application relied on under HTTP/1.1 disappeared on upgrade ([#1258](https://github.com/apache/pekko-http/pull/1258)). All three are fixed at the pinned commit, and the rule now lives in one predicate (`Rendering.isIllegalHeaderChar`) rather than in four copies. P2 as stated above is a claim about the merged enforcement path, not about the two settings. *(maintainer)
*
+**How §8 was verified.** P1, P2 and P9 were first asserted from configuration and design, then checked against the code; each had defects, all fixed at or before the pinned commit. P1: `max-chunk-count` reported the limit without stopping the parse ([#1220](https://github.com/apache/pekko-http/pull/1220)); repeated `Connection` headers escaped `max-header-count` ([#1255](https://github.com/apache/pekko-http/pull/1255)); HTTP/2 connection-level buffer accounting leaked on discarded streams until the connection window drained and every stream stalled ([#1259](https://github.com/apache/pekko-http/pull/1259), [#1281](https://github.com/apache/pekko-http/pull/1281)); nothing bounded multipart part count ([#1266](https://github.com/apache/pekko-http/pull/1266)); the HTTP/2 frame parser buffered a frame of any declared length, up to 16 MiB − 1, before any other bound applied ([#1264](https://github.com/apache/pekko-http/pull/1264)). P2: the CR/LF guard missed chunked trailers and extens
ions ([#1256](https://github.com/apache/pekko-http/pull/1256)), NUL ([#1260](https://github.com/apache/pekko-http/pull/1260)) and the HTTP/2 header path ([#1258](https://github.com/apache/pekko-http/pull/1258)). P9: a header that failed to parse desynchronised the HPACK dynamic table for every later frame ([#1252](https://github.com/apache/pekko-http/pull/1252)), and a field the HTTP/1.1 parser rejected failed the connection instead of the stream ([#1297](https://github.com/apache/pekko-http/pull/1297)). The properties as stated are claims about the merged code, not about the settings. *(maintainer)*
---
## §9 Security properties Pekko HTTP does **not** provide
-- **No claim of complete DoS resistance.** The documented wording is *"behaves pretty well under most known Denial of Service attacks"*, immediately followed by a recommendation to front it with a load balancer or enterprise routing solution *(documented — `security.md`)*. Per §14 Q1 this disclaimer is **scoped to volume**: Pekko HTTP does not claim to withstand floods, but it *does* claim that one in-limits request cannot provoke disproportionate work — that part is P1, and a violation is `VALID`. *(maintainer — §14 Q1)*
-- **No edge hardening.** Rate limiting, IP reputation, request scrubbing, connection-count throttling beyond `max-connections`, slow-loris mitigation beyond `idle-timeout` — none are provided, and none are planned. *(maintainer — §14 Q1)*
-- **No authentication or authorization.** The security directives are plumbing; the credential check is the application's function. *(maintainer — §14 Q7)*
-- **No CSRF protection.** No token issuance or verification is provided. *(maintainer — §14 Q7)*
+- **No claim of complete DoS resistance.** *"Behaves pretty well under most known Denial of Service attacks"* *(documented — `security.md`)* is scoped to **volume** (§14 Q1): Pekko HTTP does not claim to withstand floods, but does claim that one in-limits request cannot provoke disproportionate work — that is P1, and a violation is `VALID`.
+- **No edge hardening.** Rate limiting, IP reputation, request scrubbing, connection throttling beyond `max-connections`, slow-loris mitigation beyond `idle-timeout` — none provided, none planned. *(maintainer — §14 Q1)*
+- **No authentication or authorization.** The security directives are plumbing. *(maintainer — §14 Q7)*
+- **No CSRF protection.** *(maintainer — §14 Q7)*
- **No output encoding / XSS defence.** Pekko HTTP renders what the application marshals. *(maintainer — §14 Q7)*
-- **No trusted client-IP derivation from headers.** `extractClientIP` surfaces `X-Forwarded-For` / `X-Real-Ip` without validating them, and Pekko HTTP has no trusted-proxy chain configuration. `extractDirectClientIP` is the trustworthy accessor, but it yields the last proxy rather than the client. See §5 and §14 Q4.
+- **No trusted client-IP derivation from headers.** `extractClientIP` surfaces `X-Forwarded-For` / `X-Real-Ip` unvalidated; there is no trusted-proxy chain configuration. `extractDirectClientIP` is trustworthy but yields the last proxy, not the client (§14 Q4).
### False friends
-- **The security directives are not a security *system*.** `authenticateBasic` and `authenticateOAuth2` route credentials to an application-supplied verifier; they impose no password policy, no rate limiting and no lockout. They *do* supply a constant-time comparison (§8 P8) — but only to a verifier that calls `Credentials.verify`; one that pattern-matches the secret out and uses `==` gets none of it.
-- **`allowed-origins = "*"` does not mean "no credentials are exposed".** Combined with the shipped `allow-credentials = yes`, it echoes the caller's `Origin` (§5a).
+- **The security directives are not a security *system*.** `authenticateBasic` and `authenticateOAuth2` route credentials to an application-supplied verifier; no password policy, rate limiting or lockout. They supply a constant-time comparison (P8) only to a verifier that calls `Credentials.verify`.
+- **`allowed-origins = "*"` does not mean "no credentials are exposed".** With the shipped `allow-credentials = yes` it echoes the caller's `Origin` (§5a).
- **`remote-address-attribute` is not the client IP behind a proxy.** It is the socket peer.
- **`max-content-length` is not a global memory bound.** It bounds one entity; concurrent connections multiply it.
+- **`verbose-error-messages = off` is not a log-hygiene setting.** It governs what reaches the *client*. What reaches the *server log* is `error-logging-verbosity`, which ships `full`: a request whose target fails to parse has that target written to the log at warning level (`UriParser.fail`, `logParsingError`). Attacker-chosen bytes in the log at an alerting level are a log-injection and log-flooding consideration the operator owns (§10.9). *(documented — `reference.conf`, `ErrorInfo.scala`)*
Review Comment:
`ErrorInfo.scala` only stores and formats the supplied detail; it does not establish that the raw target enters the detail or that full verbosity logs it at warning level. The relevant evidence is `UriParser.fail` (copies the failing input line) plus `logParsingError`/`DefaultParsingErrorHandler` (logs `formatPretty` at warning for `full`). Cite that chain so this security claim remains verifiable.
This issue also appears on line 380 of the same file.
##########
THREAT_MODEL.md:
##########
@@ -199,65 +195,63 @@ Pekko HTTP therefore takes the following position *(maintainer)*:
**In scope:**
-- **The remote HTTP client.** Can send arbitrary bytes, malformed framing, oversized or deeply-nested input, many concurrent connections, and abusive HTTP/2 frame sequences. The primary adversary — though per §14 Q1 what this adversary achieves through sheer *volume* is the proxy's problem, not the library's. *(maintainer — §14 Q7)*
+- **The remote HTTP client.** Arbitrary bytes, malformed framing, oversized or deeply-nested input, many concurrent connections, abusive HTTP/2 frame sequences. The primary adversary — though what it achieves through sheer *volume* is the proxy's problem (§14 Q1). *(maintainer — §14 Q7)*
- **A malicious upstream server**, where the application uses the client API against an untrusted endpoint. *(maintainer — §14 Q7)*
- **A cross-origin web attacker**, where the application enables CORS. *(maintainer — §14 Q2)*
**Explicitly out of scope:**
-- **Attackers with code execution in the embedding JVM.** Already inside.
+- **Attackers with code execution in the embedding JVM.**
- **The embedding application itself.** A route that deliberately leaks data is an application bug.
-- **Side-channel observers.** No general timing guarantees are made about routing or parsing. Credential comparison is the exception: `Credentials.Provided.verify` compares in constant time (see §8 P8), so a timing finding there is in scope, while one against an application-supplied `provideVerify` is not. *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala`)*
+- **Side-channel observers.** No general timing guarantees are made about routing or parsing. Credential comparison is the exception (P8): a timing finding against `Credentials.verify` is in scope; one against an application-supplied `provideVerify` is not. *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala`)*
---
## §8 Security properties Pekko HTTP provides
| # | Property & conditions | Violation symptom | Severity | Provenance |
| --- | --- | --- | --- | --- |
-| P1 | **Inbound messages are bounded** by the §5a limits: reaching a limit stops the parse rather than merely reporting it, every header a message carries counts towards the count limits, and buffered bytes are released from the accounting when the buffer is discarded | OOM or unbounded buffering from input *within* documented limits; a limit reached without the parse stopping | **Critical** | *(documented — `reference.conf`, `HttpMessageParser.scala`, `Http2StreamHandling.scala`)* |
-| P2 | **Response splitting is blocked**: illegal response header names and values are `error` by default, and the renderers drop any header whose rendered bytes contain CR, LF or NUL — on the HTTP/1.1 main header block, on chunked-response trailers and chunk extensions, and on the HTTP/2 HPACK path alike | CRLF or NUL in an application-supplied header, trailer or chunk extension reaching the wire | **Critical** | *(documented — `reference.conf`, `Rendering.scala:270`)* |
+| P1 | **Inbound messages are bounded** by the §5a limits: reaching a limit stops the parse, every header a message carries counts towards the count limits, and buffered bytes are released from the accounting when the buffer is discarded | OOM or unbounded buffering from input *within* documented limits; a limit reached without the parse stopping | **Critical** | *(documented — `reference.conf`, `HttpMessageParser.scala`, `Http2FrameParsing.scala`, `Http2StreamHandling.scala`)* |
+| P2 | **Response splitting is blocked**: illegal response header names and values are `error` by default, and the renderers drop any header whose rendered bytes contain CR, LF or NUL — on the HTTP/1.1 header block, chunked-response trailers and chunk extensions, and the HTTP/2 HPACK path alike | CRLF or NUL in an application-supplied header, trailer or chunk extension reaching the wire | **Critical** | *(documented — `reference.conf`, `Rendering.isIllegalHeaderChar`)* |
| P3 | **Parse errors do not leak detail to the client** — `verbose-error-messages = off` | Internal parse state or stack detail in a 400 response under defaults | High | *(documented — `reference.conf`)* |
| P4 | **Strict URI and RFC6265 cookie parsing by default**, rather than lenient normalization that invites smuggling | Two components disagreeing on a URI or cookie under `strict` | High | *(documented — `reference.conf`)* |
-| P5 | **Connection and request lifetimes are bounded** — `idle-timeout 60s`, `request-timeout 20s`, `max-connections 1024`, `pipelining-limit 1` | A client holding resources indefinitely under defaults | High | *(documented — `reference.conf`)* |
-| P6 | **HTTP/2 concurrency and stream churn are bounded** — `max-concurrent-streams = 256` caps streams open at once, and `frame-type-throttle` rate-limits the frames that let a peer cycle through that cap: `RST_STREAM` is charged by default at 100 per second with a burst of 100. Concurrency alone is not a bound on *work*, because a reset frees the slot immediately — this is the HTTP/2 Rapid Reset shape (CVE-2023-44487) | Unbounded stream/state growth on one connection, or unbounded stream churn through a bounded concurrency limit | High | *(documented — `reference.conf`)* |
-| P7 | **HTTP/2 header blocks are bounded** — `max-header-list-size = 64 KiB` caps the decompressed header list *and* the accumulated HEADERS + CONTINUATION fragments, so a header block the peer never terminates with `END_HEADERS` cannot grow without bound; over-limit blocks get `GOAWAY(ENHANCE_YOUR_CALM)` rather than being buffered | Unbounded buffering from a CONTINUATION flood or an oversized header list | **Critical** | *(documented — `reference.conf`)* |
-| P8 | **Credential comparison is constant-time** where the verifier calls `Credentials.verify` — it compares via `secure_==`, which XOR-accumulates over the full length after a length check, rather than short-circuiting on the first differing byte | Secret recoverable byte-by-byte from response timing against a `verify`-based verifier | High | *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala:37`)* |
+| P5 | **Connection and request lifetimes are bounded** — `idle-timeout 60s` closes a connection with no traffic in *either* direction for that long; `request-timeout 20s` bounds handling once a request has been received; `max-connections 1024`; `pipelining-limit 1`. `idle-timeout` is an inactivity timeout, not a receive deadline (§9) | A connection with no traffic outliving `idle-timeout`; a received request outliving `request-timeout` with no response; resources held past either bound after the connection is gone | High | *(documented — `reference.conf`, `timeouts.md`)* |
Review Comment:
`request-timeout` is not started merely when the streamed request is delivered to the application. `RequestTimeoutSupport` schedules it only after `requestEnd` completes (`HttpServerBluePrint.scala:385-391`), so the current wording conflicts with §9's correct statement that trickling request bodies require an entity timeout. Scope this promise to a fully received/terminated entity.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@pekko.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@pekko.apache.org
For additional commands, e-mail: notifications-help@pekko.apache.org
Re: [PR] Re-pin the threat model to b1de40b, absorb what merged, and trim it to what a triager needs [pekko-http]
Posted by "pjfanning (via GitHub)" <gi...@apache.org>.
pjfanning commented on code in PR #1295:
URL: https://github.com/apache/pekko-http/pull/1295#discussion_r4237910464
##########
THREAT_MODEL.md:
##########
@@ -47,65 +42,63 @@ Caller roles:
| Benchmarks | `http-bench-jmh` | — | **no** — §3 |
| Lint / build / docs | `http-scalafix`, `docs`, `project`, `scripts`, `legal` | — | **no** — §3 |
-*(maintainer — the in/out split is confirmed by the Pekko maintainers; see §14 Q6)*
+*(maintainer — §14 Q6)*
---
## §3 Out of scope (explicit non-goals)
- **Test kits, benchmarks, scalafix rules, build tooling and documentation sources.** A finding in `http-bench-jmh` or any `*-tests` module is `OUT-OF-MODEL: unsupported-component`. *(maintainer — §14 Q6)*
-- **Pekko HTTP is not a WAF, and not an edge-hardened server.** The documentation says so plainly: applications *"should not be exposed to the public internet directly"* and an *"enterprise grade routing solution"* or a load balancer such as Apache HTTP Server or Nginx *"would be safer"* *(documented — `security.md`)*. See §4.
-- **Pekko HTTP is not an authentication or authorization system.** It ships `authenticateBasic`, `authenticateOAuth2` and `authorize` directives, but these are *plumbing*: the credential check is a function the application supplies. Pekko HTTP has no user store, no session model, and no policy engine. *(maintainer — §14 Q7)*
-- **The actor, stream, remoting and cluster layers** are out of scope here and covered by `apache/pekko`'s threat model.
-- **Attackers who already control the embedding process** are out of scope. *(maintainer — §14 Q7)*
+- **Pekko HTTP is not a WAF, and not an edge-hardened server.** Applications *"should not be exposed to the public internet directly"*; an *"enterprise grade routing solution"* or a load balancer such as Apache HTTP Server or Nginx *"would be safer"* *(documented — `security.md`)*. See §4.
+- **Pekko HTTP is not an authentication or authorization system.** `authenticateBasic`, `authenticateOAuth2` and `authorize` are plumbing: the credential check is a function the application supplies. There is no user store, session model or policy engine. *(maintainer — §14 Q7)*
+- **The actor, stream, remoting and cluster layers** are covered by `apache/pekko`'s threat model.
+- **Attackers who already control the embedding process.** *(maintainer — §14 Q7)*
---
## §4 Trust boundaries and data flow
**The trust boundary is the inbound HTTP request.** Everything derived from wire bytes — request line, headers, cookies, entity, HTTP/2 frames — is attacker-controlled until the application validates it.
-Pekko HTTP's documented posture is unusual and important enough to quote in full:
+Pekko HTTP's documented posture:
> "Pekko HTTP-based applications should not be exposed to the public internet directly. We believe Pekko HTTP behaves pretty well under most known Denial of Service attacks, but if you want the best security, you should use an enterprise grade routing solution. Even using a load balancing solution like an up-to-date version of Apache HTTP Server or Nginx would be safer than exposing Pekko HTTP-based applications directly to the public internet."
> — *(documented — `security.md`, "Security model")*
-Read carefully, this makes a **graded** claim rather than a binary one: Pekko HTTP asserts it *"behaves pretty well under most known"* DoS attacks — not that it is DoS-proof, and not that DoS is out of scope. That hedge was the single most consequential ambiguity in this model for triage; §14 Q1 has now resolved it into the content-vs-volume line, which is the rule triage actually applies.
+This is a graded claim, not a disclaimer of DoS as a class. §14 Q1 resolves it into the content-vs-volume line that triage applies.
### Reachability preconditions per family
-- **Core protocol / HTTP/2** — reachable from raw wire bytes. The strongest in-model surface: a parser defect here needs no application cooperation.
-- **Routing DSL** — reachable from a request that the application's own routes expose. A finding must name the directive and show a route shape a reasonable application would write.
+- **Core protocol / HTTP/2** — reachable from raw wire bytes. A parser defect here needs no application cooperation.
+- **Routing DSL** — reachable from a request the application's own routes expose. A finding must name the directive and show a route shape a reasonable application would write.
- **Marshalling** — reachable from an entity body **only where the application has bound that marshaller to a route**. Findings in the underlying JSON/XML library belong to that library.
-- **CORS** — reachable only where the application has installed the `cors()` directive. It is opt-in, not on by default. See §5a.
+- **CORS** — reachable only where the application has installed `cors()`. Opt-in, not on by default.
- **Caching** — reachable only where the application has installed a caching directive; cache-key correctness is the sharp edge.
---
## §5 Assumptions about the environment
- **Runtime.** A conformant JVM. Pekko HTTP does not defend against a hostile JVM or in-process attacker. *(maintainer — §14 Q7)*
-- **Fronting infrastructure.** The documented expectation is that something sits in front in production *(documented — `security.md`)*. Per §14 Q1 this is load-bearing for *volume* only: the proxy is relied on for flood and slow-loris defence, not for bounding a single request, which is P1's job.
-- **TLS.** Pekko HTTP can terminate TLS itself (`HttpsConnectionContext`) and this is supported, though the documented recommendation to front the service (§4) means termination is commonly the proxy's job in production. Cipher and protocol selection come from the JSSE context the application supplies — Pekko HTTP pins nothing and overrides no JDK default, so cipher strength is a deployment property. *(maintainer — §14 Q8)*
-- **Client IP.** `remote-address-attribute` ships `off` *(documented — `reference.conf`)*. When on, the attribute reflects the **socket** peer, which behind a proxy is the proxy; forwarding headers never feed it. Header-derived client IP is opt-in at the call site: `extractClientIP` reads `X-Forwarded-For` / `X-Real-Ip` and is therefore client-controllable, while `extractDirectClientIP` reads the attribute alone and is not. Choosing between them is the application's decision (§14 Q4). *(documented — `MiscDirectives.scala`)*
+- **Fronting infrastructure.** Something sits in front in production *(documented — `security.md`)*. Per §14 Q1 this is load-bearing for *volume* only: the proxy is relied on for flood and slow-loris defence, not for bounding a single request, which is P1's job.
+- **TLS.** Pekko HTTP can terminate TLS itself (`HttpsConnectionContext`); in a fronted deployment termination is commonly the proxy's job. Cipher and protocol selection come from the JSSE context the application supplies — Pekko HTTP pins nothing and overrides no JDK default. *(maintainer — §14 Q8)*
+- **Client IP.** `remote-address-attribute` ships `off` *(documented — `reference.conf`)*. When on, the attribute is the **socket** peer — behind a proxy, the proxy. `extractClientIP` reads `X-Forwarded-For` / `X-Real-Ip` and is client-controllable; `extractDirectClientIP` reads the attribute alone and is not (§14 Q4). *(documented — `MiscDirectives.scala`)*
Review Comment:
Thanks, applied. b0ca9dc69 also removes the trailing whitespace the suggestion left and adds the missing space before `MiscDirectives.scala`.
##########
THREAT_MODEL.md:
##########
@@ -199,65 +195,63 @@ Pekko HTTP therefore takes the following position *(maintainer)*:
**In scope:**
-- **The remote HTTP client.** Can send arbitrary bytes, malformed framing, oversized or deeply-nested input, many concurrent connections, and abusive HTTP/2 frame sequences. The primary adversary — though per §14 Q1 what this adversary achieves through sheer *volume* is the proxy's problem, not the library's. *(maintainer — §14 Q7)*
+- **The remote HTTP client.** Arbitrary bytes, malformed framing, oversized or deeply-nested input, many concurrent connections, abusive HTTP/2 frame sequences. The primary adversary — though what it achieves through sheer *volume* is the proxy's problem (§14 Q1). *(maintainer — §14 Q7)*
- **A malicious upstream server**, where the application uses the client API against an untrusted endpoint. *(maintainer — §14 Q7)*
- **A cross-origin web attacker**, where the application enables CORS. *(maintainer — §14 Q2)*
**Explicitly out of scope:**
-- **Attackers with code execution in the embedding JVM.** Already inside.
+- **Attackers with code execution in the embedding JVM.**
- **The embedding application itself.** A route that deliberately leaks data is an application bug.
-- **Side-channel observers.** No general timing guarantees are made about routing or parsing. Credential comparison is the exception: `Credentials.Provided.verify` compares in constant time (see §8 P8), so a timing finding there is in scope, while one against an application-supplied `provideVerify` is not. *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala`)*
+- **Side-channel observers.** No general timing guarantees are made about routing or parsing. Credential comparison is the exception (P8): a timing finding against `Credentials.verify` is in scope; one against an application-supplied `provideVerify` is not. *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala`)*
---
## §8 Security properties Pekko HTTP provides
| # | Property & conditions | Violation symptom | Severity | Provenance |
| --- | --- | --- | --- | --- |
-| P1 | **Inbound messages are bounded** by the §5a limits: reaching a limit stops the parse rather than merely reporting it, every header a message carries counts towards the count limits, and buffered bytes are released from the accounting when the buffer is discarded | OOM or unbounded buffering from input *within* documented limits; a limit reached without the parse stopping | **Critical** | *(documented — `reference.conf`, `HttpMessageParser.scala`, `Http2StreamHandling.scala`)* |
-| P2 | **Response splitting is blocked**: illegal response header names and values are `error` by default, and the renderers drop any header whose rendered bytes contain CR, LF or NUL — on the HTTP/1.1 main header block, on chunked-response trailers and chunk extensions, and on the HTTP/2 HPACK path alike | CRLF or NUL in an application-supplied header, trailer or chunk extension reaching the wire | **Critical** | *(documented — `reference.conf`, `Rendering.scala:270`)* |
+| P1 | **Inbound messages are bounded** by the §5a limits: reaching a limit stops the parse, every header a message carries counts towards the count limits, and buffered bytes are released from the accounting when the buffer is discarded | OOM or unbounded buffering from input *within* documented limits; a limit reached without the parse stopping | **Critical** | *(documented — `reference.conf`, `HttpMessageParser.scala`, `Http2FrameParsing.scala`, `Http2StreamHandling.scala`)* |
+| P2 | **Response splitting is blocked**: illegal response header names and values are `error` by default, and the renderers drop any header whose rendered bytes contain CR, LF or NUL — on the HTTP/1.1 header block, chunked-response trailers and the HTTP/2 HPACK path alike — and any chunk extension containing CR or LF | CR or LF in an application-supplied header, trailer or chunk extension, or NUL in a header or trailer, reaching the wire | **Critical** | *(documented — `reference.conf`, `Rendering.isIllegalHeaderChar`, `RenderSupport.renderChunk`)* |
| P3 | **Parse errors do not leak detail to the client** — `verbose-error-messages = off` | Internal parse state or stack detail in a 400 response under defaults | High | *(documented — `reference.conf`)* |
| P4 | **Strict URI and RFC6265 cookie parsing by default**, rather than lenient normalization that invites smuggling | Two components disagreeing on a URI or cookie under `strict` | High | *(documented — `reference.conf`)* |
-| P5 | **Connection and request lifetimes are bounded** — `idle-timeout 60s`, `request-timeout 20s`, `max-connections 1024`, `pipelining-limit 1` | A client holding resources indefinitely under defaults | High | *(documented — `reference.conf`)* |
-| P6 | **HTTP/2 concurrency and stream churn are bounded** — `max-concurrent-streams = 256` caps streams open at once, and `frame-type-throttle` rate-limits the frames that let a peer cycle through that cap: `RST_STREAM` is charged by default at 100 per second with a burst of 100. Concurrency alone is not a bound on *work*, because a reset frees the slot immediately — this is the HTTP/2 Rapid Reset shape (CVE-2023-44487) | Unbounded stream/state growth on one connection, or unbounded stream churn through a bounded concurrency limit | High | *(documented — `reference.conf`)* |
-| P7 | **HTTP/2 header blocks are bounded** — `max-header-list-size = 64 KiB` caps the decompressed header list *and* the accumulated HEADERS + CONTINUATION fragments, so a header block the peer never terminates with `END_HEADERS` cannot grow without bound; over-limit blocks get `GOAWAY(ENHANCE_YOUR_CALM)` rather than being buffered | Unbounded buffering from a CONTINUATION flood or an oversized header list | **Critical** | *(documented — `reference.conf`)* |
-| P8 | **Credential comparison is constant-time** where the verifier calls `Credentials.verify` — it compares via `secure_==`, which XOR-accumulates over the full length after a length check, rather than short-circuiting on the first differing byte | Secret recoverable byte-by-byte from response timing against a `verify`-based verifier | High | *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala:37`)* |
-
-**P1-P7 are default-on properties** — a notable contrast with `apache/pekko`, where the strongest controls must be switched on. P8 is the exception: it holds only for a verifier that calls `Credentials.verify`, which is why §10.4 states it as a downstream responsibility. The boundary of the DoS claim, once this model's largest ambiguity, is now fixed by the §14 Q1 content-vs-volume line.
-
-**A note on how P1 came to be stated this precisely.** P1 has the same history as P2, and for the same reason: the draft asserted it from the §5a table, which records what each limit is *supposed* to bound. Three of those bounds did not hold as written, all fixed at the pinned commit. `max-chunk-count` was tested without an `else` branch, so the `StateResult` of `failEntityStream` was discarded and the parser trampolined into the next chunk — reaching the limit emitted an error per remaining chunk in the buffer instead of stopping the parse ([#1220](https://github.com/apache/pekko-http/pull/1220)). `max-header-count` was not incremented on the branch that merges repeated `Connection` headers into one accumulated header, so a message could repeat that header without ever reaching the limit ([#1255](https://github.com/apache/pekko-http/pull/1255)). And on the HTTP/2 side the connection-level buffer accounting only ever went up: `totalBufferedData` was not decremented when a buffere
d stream was discarded on `RST_STREAM` or on downstream cancellation, so a peer that sent data it never let the handler read ratcheted the counter up permanently — once the leak passed half of `incoming-connection-level-buffer-size` the server stopped replenishing the connection window, it drained to zero, and every stream on that connection stalled ([#1259](https://github.com/apache/pekko-http/pull/1259)). P1 as stated above is a claim about what the parser and the frame handler do when a bound is reached, not about what the table says the bound is for. *(maintainer)*
+| P5 | **Connection and request lifetimes are bounded** — `idle-timeout 60s` closes a connection with no traffic in *either* direction for that long; `request-timeout 20s` bounds handling once a request's entity has been fully received (a body still arriving is not covered; §9); `max-connections 1024`; `pipelining-limit 1`. `idle-timeout` is an inactivity timeout, not a receive deadline (§9) | A connection with no traffic outliving `idle-timeout`; a fully received request outliving `request-timeout` with no response; resources held past either bound after the connection is gone | High | *(documented — `reference.conf`, `timeouts.md`)* |
+| P6 | **HTTP/2 concurrency and stream churn are bounded** — `max-concurrent-streams = 256` caps open streams, and `frame-type-throttle` rate-limits `RST_STREAM` (100/s, burst 100) so a peer cannot cycle through that cap for free (Rapid Reset, CVE-2023-44487) | Unbounded stream/state growth on one connection, or unbounded churn through a bounded concurrency limit | High | *(documented — `reference.conf`)* |
+| P7 | **HTTP/2 header blocks are bounded** — `max-header-list-size = 64 KiB` caps the decompressed header list *and* the accumulated HEADERS + CONTINUATION fragments; over-limit blocks get `GOAWAY(ENHANCE_YOUR_CALM)` rather than being buffered | Unbounded buffering from a CONTINUATION flood or an oversized header list | **Critical** | *(documented — `reference.conf`)* |
+| P8 | **Credential comparison is constant-time** where the verifier calls `Credentials.verify` — `secure_==` XOR-accumulates over the full length after a length check | Secret recoverable byte-by-byte from response timing against a `verify`-based verifier | High | *(documented — `SecurityDirectives.scala`, `EnhancedByteArray.scala`)* |
Review Comment:
Good catch. The leak is real and documented on `secure_==`: a length mismatch returns before the XOR loop. I agree it is hard to exploit, but the model should say so rather than leave it implied. In b0ca9dc69, P8 now states that whether the presented secret matches the stored secret's length is not hidden, and that `verify(secret, hasher)` over fixed-length digests removes even that. §14 Q5 adds that a length-only report describes documented behaviour, not a P8 violation, so a report of this shape has a disposition.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@pekko.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@pekko.apache.org
For additional commands, e-mail: notifications-help@pekko.apache.org