From 444d9399aba0dab7419978407a65ed979e9e7ccd Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Thu, 27 Aug 2026 14:13:03 +0200 Subject: [PATCH 1/9] Add a draft threat model and wire it for discoverability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Motivation The Pekko PMC asked the ASF Security team for a draft threat model to refine, as preparation for a Claude security scan. This is the companion to the same change in apache/pekko. docs/src/main/paradox/security.md already has a "Security model" section, and it makes the most important statement in this document: applications should not be exposed to the public internet directly, and DoS resistance is claimed only as "pretty well under most known" attacks. But that is three sentences, and the quantitative envelope that actually bounds untrusted input -- the parsing limits in http-core's reference.conf -- is not connected to it anywhere. Separately, the AGENTS.md -> SECURITY.md chain a scanning agent follows to locate a project's model does not resolve: there is no SECURITY.md in the repo. ### Modification - THREAT_MODEL.md: a v0 draft following the Scovetta rubric. §5a collects the shipped parsing limits, timeouts and CORS defaults as the model's quantitative spine; §8 states the six default-on properties they underwrite. - SECURITY.md: reporting policy pointing at security@apache.org, plus the three points that catch most reporters. - AGENTS.md: a Security section pointing at both. Nothing security.md asserts has been dropped or weakened; §15 back-maps each existing statement to the section that now carries it. ### Result The chain AGENTS.md -> SECURITY.md -> THREAT_MODEL.md resolves on main, and triagers have a citable document with a closed set of dispositions (§13). §14 holds 10 questions, each stated as a proposed answer. The two worth reading first are Q1 (where exactly is the DoS line, given security.md's deliberately graded claim?) and Q2 (http-cors ships allowed-origins = "*" with allow-credentials = yes, which per its own reference.conf echoes the request Origin -- deliberate, or should the default change?). The document is not ready to be treated as canonical until those are answered. ### Tests Not run - docs only ### References None - requested by the Pekko PMC chair on the ASF security scan thread --- AGENTS.md | 6 + SECURITY.md | 49 +++++++ THREAT_MODEL.md | 336 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 391 insertions(+) create mode 100644 SECURITY.md create mode 100644 THREAT_MODEL.md diff --git a/AGENTS.md b/AGENTS.md index 09388d13a..0038cc39f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -230,3 +230,9 @@ Fixes #1234, Refs #1234, or None - - Never omit `References`. - Use `Refs #...`, `Fixes #...`, or `None - `. - Do not add `Co-authored-by` or AI-assistant trailers to commits or PR descriptions. + +## Security + +- Read [`SECURITY.md`](SECURITY.md) before reporting or acting on a security issue. +- Classify security findings against the threat model it links, + [`THREAT_MODEL.md`](THREAT_MODEL.md); §11a lists recurring false positives. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..6693b2bdb --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,49 @@ +# Security Policy + +## Reporting a Vulnerability + +**Do not report security vulnerabilities through public GitHub issues, pull +requests, or the mailing lists.** + +Report them privately to the Apache Security team: + + security@apache.org + +Apache Pekko does not operate a separate project security list; reports go to +the foundation-wide address above, which routes to the Pekko PMC. + +Please follow the [guidelines laid down by the Apache Security +team](https://www.apache.org/security/). + +To receive security announcements, subscribe to the [Apache Announce Mailing +List](https://lists.apache.org/list.html?announce@apache.org). + +## Security Model + +Before reporting, please read Apache Pekko HTTP's threat model: + +[THREAT_MODEL.md](THREAT_MODEL.md) + +It states what Pekko HTTP treats as a vulnerability and what it does not — the +documented parsing limits that bound untrusted input, which configuration +defaults change the security envelope, and which properties are deliberately +left to the application. Reports that fall outside the model will be closed +citing the relevant section, so checking first will save you time. + +Three points catch most reporters: + +- **Pekko HTTP is not meant to face the public internet unaided.** The project + recommends fronting it with an enterprise-grade routing solution or a load + balancer. Denial-of-service resistance is claimed only as "pretty well under + most known attacks", not absolutely. See §4 and §14 Q1. +- **Pekko HTTP is a toolkit, not a security system.** It provides no + authentication policy, no authorization model, no CSRF protection and no + output encoding. The security directives route credentials to a verifier the + application supplies. See §9. +- **Requests rejected for exceeding a documented limit are the limits working**, + not a bug. The limits are listed in §5a. + +## Further Security Documentation + +- [Pekko HTTP security announcements](https://pekko.apache.org/docs/pekko-http/current/security.html) +- [Apache Pekko threat model](https://github.com/apache/pekko/blob/main/THREAT_MODEL.md) — the actor, stream, remoting and cluster layers this project builds on diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md new file mode 100644 index 000000000..007952f05 --- /dev/null +++ b/THREAT_MODEL.md @@ -0,0 +1,336 @@ +# Apache Pekko HTTP — Threat Model + +**Status:** DRAFT — awaiting Pekko PMC review. Not yet maintainer-ratified. + +| | | +| --- | --- | +| **Project** | Apache Pekko HTTP | +| **Written against** | commit `dd0a4a3`, `main` | +| **Date** | 2026-08-27 | +| **Authors** | ASF Security team, at the request of the Pekko PMC | +| **Version binding** | Versioned alongside the project. A report against version *N* is triaged against the model as it stood at *N*, not at `main`. | +| **Reporting** | Findings that violate a §8 property should be reported per [`SECURITY.md`](SECURITY.md). Findings under §3 or §9 will be closed citing this document. | +| **Companion model** | Pekko HTTP is built on Pekko Streams and Actors. The actor, remoting and cluster layers are modeled in [`apache/pekko`'s `THREAT_MODEL.md`](https://github.com/apache/pekko/blob/main/THREAT_MODEL.md); this document does not restate them. | + +**Provenance legend.** +*(documented)* — stated in Pekko HTTP's own docs or `reference.conf` comments, cited. +*(maintainer)* — stated by a Pekko maintainer in review of this document. +*(inferred)* — reasoned from code or config defaults, **not yet confirmed**; each has a matching question in §14. + +**Draft confidence:** 13 documented / 0 maintainer / 16 inferred — counting inline tags only. The §5a limits table and the §15 back-map carry a further ~35 documented facts under a single collective citation each, so the document is more evidence-backed than the bare ratio suggests. What is genuinely inferred clusters in three places: the §3/§7/§9 non-goals, the negative claims in §5, and the exact boundary of the DoS claim — §14 Q1, Q7 and Q9 respectively. + +Apache Pekko HTTP is a Scala/Java toolkit for building HTTP-based services and clients on top of Pekko Streams. It provides a full HTTP/1.1 and HTTP/2 implementation — parsing, connection management, marshalling, and a routing DSL of composable "directives" — as an **embeddable library**, not a standalone server. The application supplies the routes, the authentication, and the deployment. + +--- + +## §2 Scope and intended use + +Pekko HTTP is a library the application embeds. There is no Pekko HTTP daemon to secure independently of the service built on it. + +Caller roles: + +- **The embedding application** — fully trusted. Defines routes, supplies handlers, chooses configuration. +- **The operator/deployer** — trusted for the instance. Chooses what sits in front of the service (see §4), TLS termination, and limits. +- **The HTTP client** — **untrusted**. This is the adversary the model is mostly about. + +### Component families + +| Family | Modules | Entry point | In model | +| --- | --- | --- | --- | +| Core protocol | `http-core`, `parsing` | Wire bytes → `HttpRequest` / `HttpResponse` | **yes — primary surface** | +| HTTP/2 | `http2-tests` support in `http-core` | HTTP/2 framing, HPACK, streams | **yes** | +| Routing DSL | `http` | `Route`, directives, rejection/exception handling | **yes** | +| Marshalling | `http-marshallers-scala`, `http-marshallers-java` | JSON/XML entity conversion | **yes** | +| CORS | `http-cors` | `cors()` directive | **yes — see §5a** | +| Caching | `http-caching` | response cache directives | **yes** | +| Test kits | `http-testkit`, `http-testkit-munit`, `http-tests`, `http-compatibility-tests`, `http2-tests` | — | **no** — §3 | +| Benchmarks | `http-bench-jmh` | — | **no** — §3 | +| Lint / build / docs | `http-scalafix`, `docs`, `project`, `scripts`, `legal` | — | **no** — §3 | + +*(inferred — the in/out split is the ASF Security team's proposal; see §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`. *(inferred — §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. *(inferred — §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. *(inferred — §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-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 is the single most consequential ambiguity in this model for triage, and §14 Q1 asks the PMC to resolve it. + +### 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. +- **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. +- **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. *(inferred — §14 Q7)* +- **Fronting infrastructure.** The documented expectation is that something sits in front in production *(documented — `security.md`)*. §14 Q1 asks how load-bearing this is. +- **TLS.** Pekko HTTP can terminate TLS itself (`HttpsConnectionContext`), but where a reverse proxy is used, termination is commonly the proxy's job. Cipher and protocol selection come from the JSSE context the application supplies. *(inferred — §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. `X-Forwarded-For` is not trusted or parsed into it automatically — deriving client IP from headers is the application's decision. *(inferred — §14 Q4)* + +### What Pekko HTTP does not do to its host + +Negative claims, rarely written down and therefore high-priority confirmation targets *(all inferred — §14 Q9)*: + +- Binds no port until the application calls a `bind*` method. +- Installs no signal handlers, spawns no child processes. +- Writes no files of its own accord; serves from disk only via directives the application installs (`getFromFile`, `getFromDirectory`). +- Does not mutate process-global state at initialization. + +--- + +## §5a Configuration variants that change the security envelope + +Pekko HTTP's resistance to malformed and abusive input is almost entirely a function of `pekko.http.server.parsing.*`. These are the **documented, shipped** limits *(all documented — `http-core/src/main/resources/reference.conf`)*: + +| Setting | Default | What it bounds | +| --- | --- | --- | +| `max-uri-length` | `2k` | Request-line URI | +| `max-method-length` | `16` | Method token | +| `max-header-name-length` | `64` | Single header name | +| `max-header-value-length` | `8k` | Single header value | +| `max-header-count` | `64` | Headers per message | +| `max-content-length` | `8m` (server) | Entity size | +| `max-chunk-size` | `1m` | Single chunk | +| `max-chunk-count` | `100000` | Chunks per message | +| `max-chunk-ext-length` | `256` | Chunk extension | +| `max-comment-parsing-depth` | `5` | Nested comment recursion | +| `max-to-strict-bytes` | `8m` | `toStrict` materialization | +| `max-concurrent-streams` | `256` | HTTP/2 concurrent streams | +| `max-connections` | `1024` | Server connections | +| `pipelining-limit` | `1` | In-flight pipelined requests | +| `idle-timeout` | `60 s` | Connection idle | +| `request-timeout` | `20 s` | Per-request handling | +| `uri-parsing-mode` | `strict` | URI leniency | +| `cookie-parsing-mode` | `rfc6265` | Cookie leniency | +| `verbose-error-messages` | `off` | Whether parse errors leak detail to the client — **secure default** | +| `illegal-response-header-name-processing-mode` | `error` | Blocks response-splitting via header names — **secure default** | +| `illegal-response-header-value-processing-mode` | `error` | Blocks response-splitting via header values — **secure default** | +| `server-header` | `pekko-http/${version}` | Advertises product and version | +| `remote-address-attribute` | `off` | Exposes socket peer address to routes | +| `transparent-head-requests` | `off` | HEAD handled as GET | + +**These limits are the model's quantitative spine.** A report that a request *within* every documented limit causes disproportionate resource use is meaningfully different from one that simply exceeds a limit — see §14 Q1. + +### The CORS defaults — needs a ruling + +`http-cors` ships: + +| Setting | Default | +| --- | --- | +| `allowed-origins` | `"*"` | +| `allow-credentials` | `yes` | +| `allowed-headers` | `"*"` | +| `allowed-methods` | `["GET", "POST", "HEAD", "OPTIONS"]` | +| `allow-generic-http-requests` | `yes` | + +The module's own documentation states the interaction precisely: *"if parameter is `*` and credentials are not allowed, a `*` is set in `Access-Control-Allow-Origin`. Otherwise, the origins given in the `Origin` request header are echoed."* *(documented — `http-cors/reference.conf`)* + +So with **both** defaults in force, the directive **echoes the requesting `Origin` and allows credentials** — the maximally permissive CORS posture. Two facts bound how alarming that is: the `cors()` directive is **opt-in**, so this affects only applications that chose to enable CORS; and it is a documented, deliberate default rather than an accident. It is nonetheless the clearest insecure-default in the project, and §14 Q2 asks for the ruling. *(inferred that this is deliberate — §14 Q2)* + +--- + +## §6 Assumptions about inputs + +| Surface | Input | Attacker-controllable? | Who must enforce what | +| --- | --- | --- | --- | +| Any bound route | Request line (method, URI, version) | **Yes** | Pekko HTTP: §5a length limits | +| Any bound route | Headers, incl. `Host`, `Cookie` | **Yes** | Pekko HTTP: count/length limits. App: semantic trust | +| Any bound route | `X-Forwarded-*` | **Yes** — trivially spoofable | **App/operator** — not validated by Pekko HTTP (§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 marshaller | Entity parsed to a domain type | **Yes** | Underlying JSON/XML library + app | +| `cors()` | `Origin`, `Access-Control-Request-*` | **Yes** | Operator: §5a CORS config | +| File-serving directives | Path segments | **Yes** | Pekko HTTP + app — see §14 Q3 | +| Client API | Response from an upstream server | **Yes** if the upstream is untrusted | App: treat responses as untrusted | +| Config | `application.conf` | **No** — trusted deployment input | Operator | + +--- + +## §7 Adversary model + +**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. *(inferred — §14 Q1, Q7)* +- **A malicious upstream server**, where the application uses the client API against an untrusted endpoint. *(inferred — §14 Q7)* +- **A cross-origin web attacker**, where the application enables CORS. *(inferred — §14 Q2)* + +**Explicitly out of scope:** + +- **Attackers with code execution in the embedding JVM.** Already inside. +- **The embedding application itself.** A route that deliberately leaks data is an application bug. +- **Side-channel observers.** No timing guarantees are made — including, notably, that the authentication directives do **not** promise constant-time credential comparison; the comparison function is supplied by the application. *(inferred — §14 Q5)* + +--- + +## §8 Security properties Pekko HTTP provides + +| # | Property & conditions | Violation symptom | Severity | Provenance | +| --- | --- | --- | --- | --- | +| P1 | **Inbound messages are bounded** by the §5a limits; input exceeding them is rejected rather than buffered | OOM or unbounded buffering from input *within* documented limits | **Critical** | *(documented — `reference.conf`)* | +| P2 | **Response splitting is blocked**: illegal response header names and values are `error` by default, not passed through | CRLF in an application-supplied header reaching the wire | **Critical** | *(documented — `reference.conf`)* | +| 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 is bounded** — `max-concurrent-streams = 256` | Unbounded stream/state growth on one connection | High | *(documented — `reference.conf`)* | + +**Every one of these is a default-on property** — a notable contrast with `apache/pekko`, where the strongest controls must be switched on. Pekko HTTP's weak spot is not its defaults but the boundary of its DoS claim (§9, §14 Q1). + +--- + +## §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`)*. This is a hedge, not a guarantee. +- **No edge hardening.** Rate limiting, IP reputation, request scrubbing, slow-loris mitigation beyond `idle-timeout` — none are provided. *(inferred — §14 Q1)* +- **No authentication or authorization.** The security directives are plumbing; the credential check is the application's function. *(inferred — §14 Q7)* +- **No CSRF protection.** No token issuance or verification is provided. *(inferred — §14 Q7)* +- **No output encoding / XSS defence.** Pekko HTTP renders what the application marshals. *(inferred — §14 Q7)* +- **No trusted client-IP derivation.** See §5 and §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, no lockout, and — see §14 Q5 — no constant-time comparison of their own. +- **`allowed-origins = "*"` does not mean "no credentials are exposed".** Combined 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. +- **A rejection is not a failure.** The routing DSL's rejection mechanism is control flow, not a security control — an unhandled rejection can fall through to a different route. + +### Well-known attack classes left to the caller + +- **Request smuggling / desync** between a fronting proxy and Pekko HTTP — inherently a two-party property; strict parsing (P4) helps but cannot settle it alone. +- **Slow-loris and connection exhaustion** — partially bounded by P5, explicitly not fully claimed (§14 Q1). +- **Decompression bombs** in request bodies, where the application enables decoding. +- **SSRF** via the client API, where the application takes a URL from a request. +- **Path traversal** in file-serving directives — see §14 Q3. +- **XXE** in XML marshallers — a property of the underlying parser. + +--- + +## §10 Downstream responsibilities + +1. **Put an enterprise-grade proxy or load balancer in front** of an internet-facing service *(documented — `security.md`)*. +2. **Do not raise the §5a limits without understanding the memory cost** — each is multiplied by concurrent connections. +3. **If CORS is enabled, set `allowed-origins` explicitly.** Do not ship the `"*"` + `allow-credentials = yes` combination to a credentialed API (§5a). +4. **Supply a constant-time credential comparison** to the authentication directives (§14 Q5). +5. **Do not derive client identity from `X-Forwarded-For`** unless a trusted proxy sets it and the application validates the chain. +6. **Validate and canonicalize any request-derived path** before passing it to a file-serving directive. +7. **Treat client-API responses from untrusted upstreams as untrusted input.** +8. **Consider `server-header = ""`** if product/version disclosure matters to your threat model. + +--- + +## §11 Known misuse patterns + +- **Exposing a Pekko HTTP service directly to the internet** with no fronting proxy, contrary to the documented recommendation. +- **Enabling `cors()` and leaving `allowed-origins = "*"`** on an API that uses cookies or bearer tokens. +- **Trusting `X-Forwarded-For`** for rate limiting, audit logging, or access control without a trusted-proxy chain. +- **Raising `max-content-length` to `infinite`** to accept large uploads, without a concurrency bound. +- **Comparing credentials with `==`** inside an `authenticateBasic` verifier. +- **Passing a request path segment straight to `getFromFile`.** +- **Turning `verbose-error-messages = on`** in production to aid debugging. + +--- + +## §11a Known non-findings (recurring false positives) + +- **"`Server` header discloses the product and version."** Documented default, configurable via `server-header` (§5a). Not a vulnerability under this model. +- **"No authentication on routes."** Authentication is the application's responsibility (§9). A scan of this library cannot conclude a route is unauthenticated. +- **"Request exceeding `max-uri-length` / `max-header-count` is rejected."** That is P1 working. +- **"CORS allows any origin."** Reflects the shipped default and requires the application to have opted into `cors()`; pending §14 Q2, report against the *application's* configuration, not the library. +- **"`allow-credentials = yes` with `allowed-origins = "*"` sends `Access-Control-Allow-Origin: *` with credentials."** It does not — per the documented interaction it echoes the request `Origin` instead (§5a). Reports asserting the literal `*`-with-credentials combination are factually wrong. +- **Findings in `*-tests`, `http-testkit*`, `http-bench-jmh`, `http-scalafix`, `docs`** — `OUT-OF-MODEL: unsupported-component` per §3. +- **Findings in the actor or stream layer** — belongs to `apache/pekko`'s model, not this one. + +--- + +## §12 Conditions that would change this model + +- A change to any §5a **default**, especially a parsing limit, a timeout, or a CORS setting. +- A new protocol version or transport (HTTP/3). +- Taking on any authentication, authorization, or rate-limiting responsibility currently disclaimed in §9. +- A change to the documented "do not expose directly" posture in `security.md`. +- Promotion of a §3 module into the supported surface. +- **A report that cannot be routed to exactly one §13 disposition** — evidence of a model gap; revise the model rather than making an ad-hoc call. + +--- + +## §13 Triage dispositions + +| Disposition | Meaning | Licensed by | +| --- | --- | --- | +| `VALID` | Violates a §8 property via an in-scope adversary and input | §6, §7, §8 | +| `VALID-HARDENING` | No §8 property violated, but the API makes a §11 misuse easy enough to warrant hardening. Typically no CVE | §11 | +| `OUT-OF-MODEL: trusted-input` | Requires control of an input §6 marks trusted (configuration, application-supplied handlers) | §6 | +| `OUT-OF-MODEL: adversary-not-in-scope` | Requires in-JVM code execution, or a malicious embedding application | §7 | +| `OUT-OF-MODEL: unsupported-component` | Lands in a §3 module, or in the actor/stream layer | §3 | +| `OUT-OF-MODEL: non-default-build` | Only manifests under a non-default §5a setting | §5a | +| `BY-DESIGN: property-disclaimed` | Concerns a §9 property — authentication, CSRF, XSS, edge hardening | §9 | +| `KNOWN-NON-FINDING` | Matches a §11a pattern | §11a | +| `MODEL-GAP` | Routable to none of the above — triggers §12 | §12 | + +--- + +## §14 Open questions for the maintainers + +Each states a **proposed answer**; confirming or correcting is enough. + +**Q1 — Where exactly is the DoS line? (highest value — reshapes §8, §9, §11a, §13.)** +`security.md` says Pekko HTTP *"behaves pretty well under most known Denial of Service attacks"* while recommending a fronting proxy. That is deliberately graded, and triage needs a line. *Proposed:* a request **within** all §5a documented limits that causes super-linear CPU or memory is `VALID`; anything requiring limits to be raised, or requiring request *volume* rather than request *content*, is `BY-DESIGN: property-disclaimed` — volume defence is the proxy's job. Is that the intended split? + +**Q2 — The CORS defaults.** `allowed-origins = "*"` with `allow-credentials = yes` echoes the caller's `Origin` with credentials allowed. *Proposed:* deliberate, on the grounds that `cors()` is opt-in and an application enabling it is expected to configure it — so a report against the default is `VALID-HARDENING` at most, and the §10.3 guidance covers it. Confirm — or should the shipped default change? + +**Q3 — File-serving directives.** `getFromFile` / `getFromDirectory` / `getFromResourceDirectory`. *Proposed:* these perform their own containment, so a genuine traversal escaping the configured root would be `VALID`; passing an unvalidated request path in is a §11 misuse. Is containment actually claimed, and at which directive? + +**Q4 — `X-Forwarded-For` and client identity.** *Proposed:* Pekko HTTP neither parses nor trusts forwarding headers; `remote-address-attribute` is strictly the socket peer, and deriving client IP is entirely the application's job — so "forwarding header is spoofable" is `BY-DESIGN: property-disclaimed`. Confirm? + +**Q5 — Constant-time credential comparison.** Do `authenticateBasic` / `authenticateOAuth2` compare anything themselves, or is comparison entirely the application-supplied verifier's? *Proposed:* entirely the application's, so a timing-attack report against the directives is out of model and belongs in §10.4 as a downstream responsibility. Confirm? + +**Q6 — Module in/out split (§2 table).** *Proposed:* the split shown. Specifically: should `http-caching` be in model (cache-key confusion is a real class), and is `http-scalafix` correctly out? + +**Q7 — The §3/§7/§9 non-goals.** *Proposed:* Pekko HTTP provides no authentication system, no authorization policy, no CSRF protection and no XSS/output encoding, and in-JVM attackers plus a malicious embedding application are out of the adversary model — while the remote HTTP client, a malicious upstream (client API), and a cross-origin web attacker (where CORS is on) are all **in**. Confirm the split? + +**Q8 — TLS.** *Proposed:* where Pekko HTTP terminates TLS, protocol and cipher selection come from the application-supplied JSSE context, so "weak cipher accepted" is a deployment finding, not a library one. Confirm — and is in-process termination a supported production posture, or is proxy termination the expectation? + +**Q9 — The negative claims in §5.** These are inferred and hard to cite. Are any wrong — does Pekko HTTP bind ports, write files, or mutate process-global state in ways an integrator would not expect? + +**Q10 — Coexistence (meta).** `docs/src/main/paradox/security.md` has a "Security model" section that this document expands considerably. *Proposed:* this file becomes canonical for **scope and triage**, `security.md` stays canonical for **announcements and reporting**, and its "Security model" section becomes a short pointer here. Agree? + +--- + +## §15 Appendix — back-map from existing docs + +| 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 | `security.md` | §4, §9, §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 | +| `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 | +| `remote-address-attribute = off` | `http-core/reference.conf` | §5, §9, §14 Q4 | +| CORS: `*` + credentials echoes the request `Origin` | `http-cors/reference.conf` | §5a, §9, §11a, §14 Q2 | From 2d43a3be78fba7d0cbf7aeefdf8fdbfe7b598302 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Sun, 30 Aug 2026 14:07:12 +0100 Subject: [PATCH 2/9] Update THREAT_MODEL.md --- THREAT_MODEL.md | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index 007952f05..18746c6a7 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -5,7 +5,7 @@ | | | | --- | --- | | **Project** | Apache Pekko HTTP | -| **Written against** | commit `dd0a4a3`, `main` | +| **Written against** | commit `444d939`, `main` | | **Date** | 2026-08-27 | | **Authors** | ASF Security team, at the request of the Pekko PMC | | **Version binding** | Versioned alongside the project. A report against version *N* is triaged against the model as it stood at *N*, not at `main`. | @@ -17,7 +17,7 @@ *(maintainer)* — stated by a Pekko maintainer in review of this document. *(inferred)* — reasoned from code or config defaults, **not yet confirmed**; each has a matching question in §14. -**Draft confidence:** 13 documented / 0 maintainer / 16 inferred — counting inline tags only. The §5a limits table and the §15 back-map carry a further ~35 documented facts under a single collective citation each, so the document is more evidence-backed than the bare ratio suggests. What is genuinely inferred clusters in three places: the §3/§7/§9 non-goals, the negative claims in §5, and the exact boundary of the DoS claim — §14 Q1, Q7 and Q9 respectively. +**Draft confidence:** 17 documented / 0 maintainer / 15 inferred — counting inline tags only. The §5a limits table and the §15 back-map carry a further ~35 documented facts under a single collective citation each, so the document is more evidence-backed than the bare ratio suggests. What is genuinely inferred clusters in three places: the §3/§7/§9 non-goals, the negative claims in §5, and the exact boundary of the DoS claim — §14 Q1, Q7 and Q9 respectively. §14 Q3 and Q5 were originally inferred and have since been resolved against the source; they remain listed so the PMC can confirm the *disposition*, not the fact. Apache Pekko HTTP is a Scala/Java toolkit for building HTTP-based services and clients on top of Pekko Streams. It provides a full HTTP/1.1 and HTTP/2 implementation — parsing, connection management, marshalling, and a routing DSL of composable "directives" — as an **embeddable library**, not a standalone server. The application supplies the routes, the authentication, and the deployment. @@ -118,6 +118,7 @@ Pekko HTTP's resistance to malformed and abusive input is almost entirely a func | `max-comment-parsing-depth` | `5` | Nested comment recursion | | `max-to-strict-bytes` | `8m` | `toStrict` materialization | | `max-concurrent-streams` | `256` | HTTP/2 concurrent streams | +| `max-header-list-size` | `64 KiB` | HTTP/2 decompressed header list, **and** the accumulated HEADERS + CONTINUATION fragments for one header block | | `max-connections` | `1024` | Server connections | | `pipelining-limit` | `1` | In-flight pipelined requests | | `idle-timeout` | `60 s` | Connection idle | @@ -180,7 +181,7 @@ So with **both** defaults in force, the directive **echoes the requesting `Origi - **Attackers with code execution in the embedding JVM.** Already inside. - **The embedding application itself.** A route that deliberately leaks data is an application bug. -- **Side-channel observers.** No timing guarantees are made — including, notably, that the authentication directives do **not** promise constant-time credential comparison; the comparison function is supplied by the application. *(inferred — §14 Q5)* +- **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`)* --- @@ -194,8 +195,10 @@ So with **both** defaults in force, the directive **echoes the requesting `Origi | 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 is bounded** — `max-concurrent-streams = 256` | Unbounded stream/state growth on one connection | 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`)* | -**Every one of these is a default-on property** — a notable contrast with `apache/pekko`, where the strongest controls must be switched on. Pekko HTTP's weak spot is not its defaults but the boundary of its DoS claim (§9, §14 Q1). +**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. Pekko HTTP's weak spot is not its defaults but the boundary of its DoS claim (§9, §14 Q1). --- @@ -210,7 +213,7 @@ So with **both** defaults in force, the directive **echoes the requesting `Origi ### 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, no lockout, and — see §14 Q5 — no constant-time comparison of their own. +- **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). - **`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. @@ -222,7 +225,7 @@ So with **both** defaults in force, the directive **echoes the requesting `Origi - **Slow-loris and connection exhaustion** — partially bounded by P5, explicitly not fully claimed (§14 Q1). - **Decompression bombs** in request bodies, where the application enables decoding. - **SSRF** via the client API, where the application takes a URL from a request. -- **Path traversal** in file-serving directives — see §14 Q3. +- **Path traversal** in file-serving directives is *not* left to the caller — `safeDirectoryChildPath` contains it (§14 Q3). What remains the caller's is the surrounding choice: which root is served, and whether a symlink may point out of it. - **XXE** in XML marshallers — a property of the underlying parser. --- @@ -232,7 +235,7 @@ So with **both** defaults in force, the directive **echoes the requesting `Origi 1. **Put an enterprise-grade proxy or load balancer in front** of an internet-facing service *(documented — `security.md`)*. 2. **Do not raise the §5a limits without understanding the memory cost** — each is multiplied by concurrent connections. 3. **If CORS is enabled, set `allowed-origins` explicitly.** Do not ship the `"*"` + `allow-credentials = yes` combination to a credentialed API (§5a). -4. **Supply a constant-time credential comparison** to the authentication directives (§14 Q5). +4. **Compare credentials with `Credentials.verify`**, which is constant-time (§8 P8) — not with `==` on the secret, and not via `provideVerify` unless the supplied verifier is itself constant-time. 5. **Do not derive client identity from `X-Forwarded-For`** unless a trusted proxy sets it and the application validates the chain. 6. **Validate and canonicalize any request-derived path** before passing it to a file-serving directive. 7. **Treat client-API responses from untrusted upstreams as untrusted input.** @@ -246,7 +249,7 @@ So with **both** defaults in force, the directive **echoes the requesting `Origi - **Enabling `cors()` and leaving `allowed-origins = "*"`** on an API that uses cookies or bearer tokens. - **Trusting `X-Forwarded-For`** for rate limiting, audit logging, or access control without a trusted-proxy chain. - **Raising `max-content-length` to `infinite`** to accept large uploads, without a concurrency bound. -- **Comparing credentials with `==`** inside an `authenticateBasic` verifier. +- **Comparing credentials with `==`** inside an `authenticateBasic` verifier — or reaching for `provideVerify` with a non-constant-time verifier — instead of `Credentials.verify`. - **Passing a request path segment straight to `getFromFile`.** - **Turning `verbose-error-messages = on`** in production to aid debugging. @@ -258,7 +261,8 @@ So with **both** defaults in force, the directive **echoes the requesting `Origi - **"No authentication on routes."** Authentication is the application's responsibility (§9). A scan of this library cannot conclude a route is unauthenticated. - **"Request exceeding `max-uri-length` / `max-header-count` is rejected."** That is P1 working. - **"CORS allows any origin."** Reflects the shipped default and requires the application to have opted into `cors()`; pending §14 Q2, report against the *application's* configuration, not the library. -- **"`allow-credentials = yes` with `allowed-origins = "*"` sends `Access-Control-Allow-Origin: *` with credentials."** It does not — per the documented interaction it echoes the request `Origin` instead (§5a). Reports asserting the literal `*`-with-credentials combination are factually wrong. +- **"`allow-credentials = yes` with `allowed-origins = "*"` sends `Access-Control-Allow-Origin: *` with credentials."** It does not — the literal `*` is sent only when `allowCredentials` is false, otherwise the request `Origin` is echoed (`CorsSettingsImpl.scala:64`, covered by `CorsDirectivesSpec`). Reports asserting the literal `*`-with-credentials combination are factually wrong. +- **"Credential comparison is vulnerable to a timing attack."** Check which comparator the report exercises: `Credentials.verify` is constant-time (§8 P8), so the claim is wrong against it; against an application's own `provideVerify` comparator it is a finding in that application, not this library. - **Findings in `*-tests`, `http-testkit*`, `http-bench-jmh`, `http-scalafix`, `docs`** — `OUT-OF-MODEL: unsupported-component` per §3. - **Findings in the actor or stream layer** — belongs to `apache/pekko`'s model, not this one. @@ -300,11 +304,11 @@ Each states a **proposed answer**; confirming or correcting is enough. **Q2 — The CORS defaults.** `allowed-origins = "*"` with `allow-credentials = yes` echoes the caller's `Origin` with credentials allowed. *Proposed:* deliberate, on the grounds that `cors()` is opt-in and an application enabling it is expected to configure it — so a report against the default is `VALID-HARDENING` at most, and the §10.3 guidance covers it. Confirm — or should the shipped default change? -**Q3 — File-serving directives.** `getFromFile` / `getFromDirectory` / `getFromResourceDirectory`. *Proposed:* these perform their own containment, so a genuine traversal escaping the configured root would be `VALID`; passing an unvalidated request path in is a §11 misuse. Is containment actually claimed, and at which directive? +**Q3 — File-serving directives.** *Resolved from code — confirm the disposition only.* `safeDirectoryChildPath` contains traversal by two stated measures: a path segment must not be `..` and must not contain `/` or `\\`; and the resolved file's `File.getCanonicalPath` must be prefixed by the base path's. So containment **is** claimed, and a genuine escape from the configured root is `VALID`; passing an unvalidated path in is a §11 misuse. One residual the code comment itself flags: containment rests on `getCanonicalPath`, whose symlink resolution is platform-dependent — *is a symlink out of the served root a `VALID` finding, or an operator responsibility?* *(documented — `FileAndResourceDirectives.scala:229-274`)* **Q4 — `X-Forwarded-For` and client identity.** *Proposed:* Pekko HTTP neither parses nor trusts forwarding headers; `remote-address-attribute` is strictly the socket peer, and deriving client IP is entirely the application's job — so "forwarding header is spoofable" is `BY-DESIGN: property-disclaimed`. Confirm? -**Q5 — Constant-time credential comparison.** Do `authenticateBasic` / `authenticateOAuth2` compare anything themselves, or is comparison entirely the application-supplied verifier's? *Proposed:* entirely the application's, so a timing-attack report against the directives is out of model and belongs in §10.4 as a downstream responsibility. Confirm? +**Q5 — Constant-time credential comparison.** *Resolved from code — this document's earlier draft had it backwards.* `Credentials.Provided.verify` does compare, via `secure_==` (`EnhancedByteArray.scala:37`), which is constant-time; the library therefore **does** provide the guarantee, recorded as §8 P8. It is conditional on the verifier calling `verify` — `provideVerify` hands the raw secret to application code and waives it. *Proposed:* a timing finding against `verify` is `VALID`; one against an application's own `provideVerify` comparator is `BY-DESIGN: property-disclaimed`. Confirm the split? **Q6 — Module in/out split (§2 table).** *Proposed:* the split shown. Specifically: should `http-caching` be in model (cache-key confusion is a real class), and is `http-scalafix` correctly out? @@ -332,5 +336,8 @@ Each states a **proposed answer**; confirming or correcting is enough. | `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 | +| `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 a canonical-path prefix | `FileAndResourceDirectives.scala` | §9, §14 Q3 | | `remote-address-attribute = off` | `http-core/reference.conf` | §5, §9, §14 Q4 | | CORS: `*` + credentials echoes the request `Origin` | `http-cors/reference.conf` | §5a, §9, §11a, §14 Q2 | From a6d7a2d278cdc4e1533aeca163b97015b844ed65 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Sun, 30 Aug 2026 14:20:40 +0100 Subject: [PATCH 3/9] =?UTF-8?q?Answer=20Q1=20and=20Q2,=20and=20record=20th?= =?UTF-8?q?e=20defaults=20posture=20as=20=C2=A75b?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation: The draft threat model left ten questions open in §14. Two of them were not lookups but rulings only the PMC could make, and until they were settled a triager had no line to apply: Q1, the boundary of the "behaves pretty well under most known DoS attacks" claim, and Q2, the CORS defaults. Separately, the model had no statement of the project's standing position on configuration defaults, so every "this default should be stricter" report had to be argued from scratch. Modification: - §14 Q1 answered: the DoS line is content vs. volume. A single request within every §5a limit that provokes super-linear CPU or memory is VALID; a finding needing a limit raised is OUT-OF-MODEL: non-default-build; one depending on request volume is BY-DESIGN: property-disclaimed. Propagated to §4, §5, §5a, §7, §8, §9, §11a and §13. - New §5b, "Security posture: hardening, not secure-by-default", mirroring apache/pekko#3478 §5b: defaults are compatibility choices, a request to tighten one is a change request not a vulnerability, proposals belong on the development list, and an implementation that does not do what it documents is a defect this project fixes. - §14 Q2 answered from §5b: the CORS pair is a compatibility default, inherited with the code donated by Lomig Mégard (legal/CorsNotice.txt). A request to change it is BY-DESIGN: default-configuration; a cors() directive that admits an origin its configuration should have rejected remains in scope. - New §13 disposition BY-DESIGN: default-configuration, disambiguated from OUT-OF-MODEL: non-default-build (a real defect reachable only off-default, versus no defect at all). - SECURITY.md states both rulings for reporters, and closes on what the project does want: an implementation that does not match its documentation. Result: Resource exhaustion and default-configuration reports now route to exactly one §13 disposition instead of being judgment calls. Four of the ten §14 questions are closed; the six remaining are confirm-or-correct. Provenance moves from 17 documented / 0 maintainer / 15 inferred to 17 / 6 / 13. Tests: Not run - docs only References: Refs #1242 --- SECURITY.md | 22 +++++++++++++++-- THREAT_MODEL.md | 66 +++++++++++++++++++++++++++++++++---------------- 2 files changed, 65 insertions(+), 23 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 6693b2bdb..adca613c3 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -34,14 +34,32 @@ Three points catch most reporters: - **Pekko HTTP is not meant to face the public internet unaided.** The project recommends fronting it with an enterprise-grade routing solution or a load - balancer. Denial-of-service resistance is claimed only as "pretty well under - most known attacks", not absolutely. See §4 and §14 Q1. + balancer. For denial of service the model draws an explicit line: a **single + request within every documented limit** that causes disproportionate CPU or + memory use is a vulnerability, and we want that report. Resource exhaustion + that depends on request **volume** — connection floods, slow-loris at scale — + is the fronting proxy's job and will be closed as by-design. Load-generator + output is not a finding. See §5a for the limits and §14 Q1 for the ruling. - **Pekko HTTP is a toolkit, not a security system.** It provides no authentication policy, no authorization model, no CSRF protection and no output encoding. The security directives route credentials to a verifier the application supplies. See §9. - **Requests rejected for exceeding a documented limit are the limits working**, not a bug. The limits are listed in §5a. +- **Configuration defaults are compatibility choices, not security claims.** Pekko + HTTP inherits a large deployment base from Akka HTTP, and tightening a shipped + default breaks working deployments on upgrade. A report that a default *should* + be stricter is a change request, not a vulnerability, and will be closed as + by-design — but it is genuinely welcome on the development list, where the PMC + will weigh it on its merits. You are free to strengthen any of these settings in + your own configuration, and §10 says which ones matter most. See §5b. + +**What we do want.** The flip side of the above is the report this project values +most: **an implementation that does not do what it is documented to do.** If a +parsing limit fails to bound what it claims to bound, if a control that is switched +on can be bypassed, if a containment check can be walked around — that is a defect, +it is in scope, and we will fix it. The defaults debate is about which value ships; +it never excuses a mechanism that does not work. ## Further Security Documentation diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index 18746c6a7..a18d10fda 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -1,6 +1,6 @@ # Apache Pekko HTTP — Threat Model -**Status:** DRAFT — awaiting Pekko PMC review. Not yet maintainer-ratified. +**Status:** DRAFT — awaiting Pekko PMC review. Not yet ratified as a whole. **§14 Q1 (the DoS line) and Q2 (the CORS defaults) have been answered by a maintainer**, and §5b records the project's standing position on configuration defaults; these are settled model. The remaining questions in §14 are still open. | | | | --- | --- | @@ -17,7 +17,7 @@ *(maintainer)* — stated by a Pekko maintainer in review of this document. *(inferred)* — reasoned from code or config defaults, **not yet confirmed**; each has a matching question in §14. -**Draft confidence:** 17 documented / 0 maintainer / 15 inferred — counting inline tags only. The §5a limits table and the §15 back-map carry a further ~35 documented facts under a single collective citation each, so the document is more evidence-backed than the bare ratio suggests. What is genuinely inferred clusters in three places: the §3/§7/§9 non-goals, the negative claims in §5, and the exact boundary of the DoS claim — §14 Q1, Q7 and Q9 respectively. §14 Q3 and Q5 were originally inferred and have since been resolved against the source; they remain listed so the PMC can confirm the *disposition*, not the fact. +**Draft confidence:** 17 documented / 6 maintainer / 13 inferred — counting inline tags only. The §5a limits table and the §15 back-map carry a further ~35 documented facts under a single collective citation each, so the document is more evidence-backed than the bare ratio suggests. What is genuinely inferred now clusters in two places: the §3/§7/§9 non-goals and the negative claims in §5 — §14 Q7 and Q9 respectively. Four questions are closed: Q1 and Q2 answered by a maintainer, Q3 and Q5 resolved against the source (they remain listed so the PMC can confirm the *disposition*, not the fact). The DoS boundary, previously the largest inferred area, is now maintainer-settled. Apache Pekko HTTP is a Scala/Java toolkit for building HTTP-based services and clients on top of Pekko Streams. It provides a full HTTP/1.1 and HTTP/2 implementation — parsing, connection management, marshalling, and a routing DSL of composable "directives" — as an **embeddable library**, not a standalone server. The application supplies the routes, the authentication, and the deployment. @@ -70,7 +70,7 @@ Pekko HTTP's documented posture is unusual and important enough to quote in full > "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 is the single most consequential ambiguity in this model for triage, and §14 Q1 asks the PMC to resolve it. +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. ### Reachability preconditions per family @@ -85,7 +85,7 @@ Read carefully, this makes a **graded** claim rather than a binary one: Pekko HT ## §5 Assumptions about the environment - **Runtime.** A conformant JVM. Pekko HTTP does not defend against a hostile JVM or in-process attacker. *(inferred — §14 Q7)* -- **Fronting infrastructure.** The documented expectation is that something sits in front in production *(documented — `security.md`)*. §14 Q1 asks how load-bearing this is. +- **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`), but where a reverse proxy is used, termination is commonly the proxy's job. Cipher and protocol selection come from the JSSE context the application supplies. *(inferred — §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. `X-Forwarded-For` is not trusted or parsed into it automatically — deriving client IP from headers is the application's decision. *(inferred — §14 Q4)* @@ -132,9 +132,9 @@ Pekko HTTP's resistance to malformed and abusive input is almost entirely a func | `remote-address-attribute` | `off` | Exposes socket peer address to routes | | `transparent-head-requests` | `off` | HEAD handled as GET | -**These limits are the model's quantitative spine.** A report that a request *within* every documented limit causes disproportionate resource use is meaningfully different from one that simply exceeds a limit — see §14 Q1. +**These limits are the model's quantitative spine.** A report that a request *within* every documented limit causes disproportionate resource use is `VALID`; one that simply exceeds a limit is P1 working, and one that needs a limit raised is `OUT-OF-MODEL: non-default-build` (§14 Q1). -### The CORS defaults — needs a ruling +### The CORS defaults — ruled (§5b, §14 Q2) `http-cors` ships: @@ -148,7 +148,22 @@ Pekko HTTP's resistance to malformed and abusive input is almost entirely a func The module's own documentation states the interaction precisely: *"if parameter is `*` and credentials are not allowed, a `*` is set in `Access-Control-Allow-Origin`. Otherwise, the origins given in the `Origin` request header are echoed."* *(documented — `http-cors/reference.conf`)* -So with **both** defaults in force, the directive **echoes the requesting `Origin` and allows credentials** — the maximally permissive CORS posture. Two facts bound how alarming that is: the `cors()` directive is **opt-in**, so this affects only applications that chose to enable CORS; and it is a documented, deliberate default rather than an accident. It is nonetheless the clearest insecure-default in the project, and §14 Q2 asks for the ruling. *(inferred that this is deliberate — §14 Q2)* +So with **both** defaults in force, the directive **echoes the requesting `Origin` and allows credentials** — the maximally permissive CORS posture. Two facts bound how alarming that is: the `cors()` directive is **opt-in**, so this affects only applications that chose to enable CORS; and it is a documented, deliberate default rather than an accident. It is nonetheless the most permissive default in the project. Its disposition follows §5b: the shipped value is a compatibility choice, and a request to change it is not a vulnerability report (§14 Q2). *(maintainer — §14 Q2)* + +--- + +## §5b Security posture: hardening, not secure-by-default + +Pekko HTTP is a long-lived toolkit whose deployment base is inherited from Akka HTTP, and `http-cors` carries a second inheritance on top of that — its defaults arrived with the code donated by Lomig Mégard (`legal/CorsNotice.txt`). Its configuration defaults are chosen for compatibility with those deployments, in which operators have already been tasked with fronting the service (§4, §10.1) and choosing limits appropriate to their traffic. Changing a default to a more restrictive value breaks those deployments on upgrade, sometimes without a clear signal as to why — a tightened parsing limit surfaces as requests that used to work now failing with a 4xx. + +Pekko HTTP therefore takes the following position *(maintainer)*: + +1. **Defaults are compatibility choices, not security claims.** §5a lists every setting whose default affects the security envelope; §10 lists what the operator must do as a result. Read together they are the contract: Pekko HTTP states what it does not provide, and states what it expects of the operator instead. +2. **A report that a default should be more restrictive is not a vulnerability report.** It is a change request, and is closed as `BY-DESIGN: default-configuration` (§13). This covers the recurring ones: the CORS pair in §5a, `server-header` disclosing a version, and every limit someone considers too generous. +3. **Proposals to change a default are welcome, and belong on the development list.** The PMC will weigh them in good faith on their merits — the compatibility cost, whether a migration path exists, and whether a major version is in flight. Defaults can and do change; they change through project discussion, not as the remediation of a security report. +4. **If an implementation is wrong, Pekko HTTP fixes it.** Where a control does not do what it is documented to do once enabled, that is a defect, in scope, at the severity §8 assigns. **This is the sharp end of the model.** The §5a limits and the §8 properties are only worth what their implementations deliver: a `max-header-list-size` that fails to bound CONTINUATION accumulation, an `illegal-response-header-value-processing-mode = error` that lets a CRLF through, a `safeDirectoryChildPath` that can be walked out of — those are the findings this project wants. This posture governs which value ships as the default — never whether the mechanism works. + +**Users are free to strengthen any §5a setting**, and §10 says which ones matter most. What the project will not do is change the shipped value on their behalf. --- @@ -173,7 +188,7 @@ So with **both** defaults in force, the directive **echoes the requesting `Origi **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. *(inferred — §14 Q1, Q7)* +- **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. *(inferred — §14 Q7)* - **A malicious upstream server**, where the application uses the client API against an untrusted endpoint. *(inferred — §14 Q7)* - **A cross-origin web attacker**, where the application enables CORS. *(inferred — §14 Q2)* @@ -198,14 +213,14 @@ So with **both** defaults in force, the directive **echoes the requesting `Origi | 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. Pekko HTTP's weak spot is not its defaults but the boundary of its DoS claim (§9, §14 Q1). +**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. --- ## §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`)*. This is a hedge, not a guarantee. -- **No edge hardening.** Rate limiting, IP reputation, request scrubbing, slow-loris mitigation beyond `idle-timeout` — none are provided. *(inferred — §14 Q1)* +- **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. *(inferred — §14 Q7)* - **No CSRF protection.** No token issuance or verification is provided. *(inferred — §14 Q7)* - **No output encoding / XSS defence.** Pekko HTTP renders what the application marshals. *(inferred — §14 Q7)* @@ -222,7 +237,7 @@ So with **both** defaults in force, the directive **echoes the requesting `Origi ### Well-known attack classes left to the caller - **Request smuggling / desync** between a fronting proxy and Pekko HTTP — inherently a two-party property; strict parsing (P4) helps but cannot settle it alone. -- **Slow-loris and connection exhaustion** — partially bounded by P5, explicitly not fully claimed (§14 Q1). +- **Slow-loris and connection exhaustion** — `idle-timeout` and `max-connections` (P5) bound what one connection holds and how many are accepted, but exhausting those bounds by volume is disclaimed (§14 Q1). A single connection that evades `idle-timeout` while holding resources is the in-scope version. - **Decompression bombs** in request bodies, where the application enables decoding. - **SSRF** via the client API, where the application takes a URL from a request. - **Path traversal** in file-serving directives is *not* left to the caller — `safeDirectoryChildPath` contains it (§14 Q3). What remains the caller's is the surrounding choice: which root is served, and whether a symlink may point out of it. @@ -257,10 +272,11 @@ So with **both** defaults in force, the directive **echoes the requesting `Origi ## §11a Known non-findings (recurring false positives) -- **"`Server` header discloses the product and version."** Documented default, configurable via `server-header` (§5a). Not a vulnerability under this model. +- **"`Server` header discloses the product and version."** Documented default, configurable via `server-header` (§5a). Not a vulnerability under this model; a request to blank it by default is `BY-DESIGN: default-configuration` per §5b. - **"No authentication on routes."** Authentication is the application's responsibility (§9). A scan of this library cannot conclude a route is unauthenticated. - **"Request exceeding `max-uri-length` / `max-header-count` is rejected."** That is P1 working. -- **"CORS allows any origin."** Reflects the shipped default and requires the application to have opted into `cors()`; pending §14 Q2, report against the *application's* configuration, not the library. +- **"N concurrent connections / requests exhaust CPU, memory or sockets."** Volume-based resource exhaustion is `BY-DESIGN: property-disclaimed` per §14 Q1 — defence belongs to the fronting proxy (§10.1). Reports must show *one* in-limits request doing disproportionate work, not many requests doing proportionate work. A load-generator result is not a finding. +- **"CORS allows any origin."** Reflects the shipped default and requires the application to have opted into `cors()`. A request to change the default is `BY-DESIGN: default-configuration` per §5b; a misconfigured deployment is a finding against the *application*, not the library. - **"`allow-credentials = yes` with `allowed-origins = "*"` sends `Access-Control-Allow-Origin: *` with credentials."** It does not — the literal `*` is sent only when `allowCredentials` is false, otherwise the request `Origin` is echoed (`CorsSettingsImpl.scala:64`, covered by `CorsDirectivesSpec`). Reports asserting the literal `*`-with-credentials combination are factually wrong. - **"Credential comparison is vulnerable to a timing attack."** Check which comparator the report exercises: `Credentials.verify` is constant-time (§8 P8), so the claim is wrong against it; against an application's own `provideVerify` comparator it is a finding in that application, not this library. - **Findings in `*-tests`, `http-testkit*`, `http-bench-jmh`, `http-scalafix`, `docs`** — `OUT-OF-MODEL: unsupported-component` per §3. @@ -283,13 +299,14 @@ So with **both** defaults in force, the directive **echoes the requesting `Origi | Disposition | Meaning | Licensed by | | --- | --- | --- | -| `VALID` | Violates a §8 property via an in-scope adversary and input | §6, §7, §8 | +| `VALID` | Violates a §8 property via an in-scope adversary and input. For resource exhaustion: a **single request within all §5a limits** causing super-linear CPU or memory | §6, §7, §8, §14 Q1 | | `VALID-HARDENING` | No §8 property violated, but the API makes a §11 misuse easy enough to warrant hardening. Typically no CVE | §11 | | `OUT-OF-MODEL: trusted-input` | Requires control of an input §6 marks trusted (configuration, application-supplied handlers) | §6 | | `OUT-OF-MODEL: adversary-not-in-scope` | Requires in-JVM code execution, or a malicious embedding application | §7 | | `OUT-OF-MODEL: unsupported-component` | Lands in a §3 module, or in the actor/stream layer | §3 | -| `OUT-OF-MODEL: non-default-build` | Only manifests under a non-default §5a setting | §5a | -| `BY-DESIGN: property-disclaimed` | Concerns a §9 property — authentication, CSRF, XSS, edge hardening | §9 | +| `OUT-OF-MODEL: non-default-build` | Only manifests under a non-default §5a setting — including any resource-exhaustion report that needs a limit **raised** from its default. Distinct from `default-configuration`: this is a real defect reachable only off-default, that one is no defect at all | §5a, §14 Q1 | +| `BY-DESIGN: default-configuration` | Asks that a §5a default be changed to a more restrictive value. Not a vulnerability; §5b.3 invites the proposal on the development list | §5b | +| `BY-DESIGN: property-disclaimed` | Concerns a §9 property — authentication, CSRF, XSS, edge hardening — or depends on request **volume** rather than request **content** | §9, §14 Q1 | | `KNOWN-NON-FINDING` | Matches a §11a pattern | §11a | | `MODEL-GAP` | Routable to none of the above — triggers §12 | §12 | @@ -299,10 +316,16 @@ So with **both** defaults in force, the directive **echoes the requesting `Origi Each states a **proposed answer**; confirming or correcting is enough. -**Q1 — Where exactly is the DoS line? (highest value — reshapes §8, §9, §11a, §13.)** -`security.md` says Pekko HTTP *"behaves pretty well under most known Denial of Service attacks"* while recommending a fronting proxy. That is deliberately graded, and triage needs a line. *Proposed:* a request **within** all §5a documented limits that causes super-linear CPU or memory is `VALID`; anything requiring limits to be raised, or requiring request *volume* rather than request *content*, is `BY-DESIGN: property-disclaimed` — volume defence is the proxy's job. Is that the intended split? +**Q1 — Where exactly is the DoS line? — ANSWERED.** *(maintainer)* +The proposed split was accepted as written. The **DoS line** is now settled model, restated here as the canonical form and applied throughout §4, §5a, §8, §9, §11a and §13: + +> A **single request within every §5a documented limit** that provokes super-linear CPU or memory is `VALID` — this is the P1 violation symptom. +> A finding that requires a §5a limit to be **raised** from its shipped default is `OUT-OF-MODEL: non-default-build`. +> A finding that depends on request **volume** rather than request **content** — connection floods, slow-loris at scale, aggregate bandwidth — is `BY-DESIGN: property-disclaimed`. Volume defence is the fronting proxy's job (§10.1). + +The operative test is *content vs. volume*: one well-formed, in-limits request doing disproportionate work is a bug in Pekko HTTP; many requests doing proportionate work is a deployment concern. -**Q2 — The CORS defaults.** `allowed-origins = "*"` with `allow-credentials = yes` echoes the caller's `Origin` with credentials allowed. *Proposed:* deliberate, on the grounds that `cors()` is opt-in and an application enabling it is expected to configure it — so a report against the default is `VALID-HARDENING` at most, and the §10.3 guidance covers it. Confirm — or should the shipped default change? +**Q2 — The CORS defaults. ANSWERED *(maintainer)*.** **Answer:** a compatibility default under §5b. The values arrived with the donated `http-cors` code and existing users depend on them; `cors()` is opt-in, and an application that enables it is expected to configure it, with §10.3 stating what to set. A report that the shipped default should change is `BY-DESIGN: default-configuration` — welcome on the development list, not as a security report. What *is* in scope is the implementation: if `cors()` admits an origin its configuration should have rejected, or emits credentials for one it should not, that is a defect under §5b.4. **Q3 — File-serving directives.** *Resolved from code — confirm the disposition only.* `safeDirectoryChildPath` contains traversal by two stated measures: a path segment must not be `..` and must not contain `/` or `\\`; and the resolved file's `File.getCanonicalPath` must be prefixed by the base path's. So containment **is** claimed, and a genuine escape from the configured root is `VALID`; passing an unvalidated path in is a §11 misuse. One residual the code comment itself flags: containment rests on `getCanonicalPath`, whose symlink resolution is platform-dependent — *is a symlink out of the served root a `VALID` finding, or an operator responsibility?* *(documented — `FileAndResourceDirectives.scala:229-274`)* @@ -327,7 +350,7 @@ Each states a **proposed answer**; confirming or correcting is enough. | 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 | `security.md` | §4, §9, §14 Q1 | +| 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 | | 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 | @@ -341,3 +364,4 @@ Each states a **proposed answer**; confirming or correcting is enough. | `safeDirectoryChildPath` rejects `..`/separator segments and enforces a canonical-path prefix | `FileAndResourceDirectives.scala` | §9, §14 Q3 | | `remote-address-attribute = off` | `http-core/reference.conf` | §5, §9, §14 Q4 | | CORS: `*` + credentials echoes the request `Origin` | `http-cors/reference.conf` | §5a, §9, §11a, §14 Q2 | +| `http-cors` code donated by Lomig Mégard, defaults inherited with it | `legal/CorsNotice.txt`, `NOTICE` | §5b, §14 Q2 | From 3c5559c5fc10b317aec3c03899fbb801b7fe1487 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Sun, 30 Aug 2026 14:28:43 +0100 Subject: [PATCH 4/9] Answer Q4-Q8, resolve Q9 against the source, correct the client-IP claim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation: Six of the ten §14 questions were still open, and two of them rested on statements that the source contradicts. Q4 asserted that Pekko HTTP "neither parses nor trusts forwarding headers", which is not what extractClientIP does. Q9 held §5's negative claims as the document's last uncited assertions, and the equivalent section in apache/pekko#3478 turned out to be wrong on review, so asserting rather than checking them was not good enough here either. Modification: - Q4 answered, and its stated basis corrected: extractClientIP resolves X-Forwarded-For (first address) -> X-Real-Ip -> the remoteAddress attribute (MiscDirectives.scala:142-145), so forwarding headers are parsed, just never implicitly. The spoofability is documented at the directive and answered by extractDirectClientIP (#1219), which reads the attribute alone. Disposition is unchanged: BY-DESIGN: property-disclaimed, with a defect in extractDirectClientIP being VALID. Propagated to §5, §6, §9, §10.5, §11, §11a. - Q5, Q6, Q7 and Q8 answered as proposed: the verify/provideVerify split, the §2 module in/out split, the non-goals and adversary split, and TLS cipher selection as a deployment property. Q8 additionally records that in-process termination is supported while the fronted posture is what the docs recommend. - Q9 resolved by scanning the main sources of http-core, http, parsing, http-caching and http-cors: no addShutdownHook, ProcessBuilder/Runtime.exec, Signal/SignalHandler, file-writing API, System.setProperty/Security.*, or bind outside the public Http().bind* entry points. §5 now cites the scan, and records the inherited caveat that the ActorSystem registers shutdown hooks Pekko HTTP does not. Result: No claim in the document is inferred any more; provenance moves from 17 documented / 6 maintainer / 13 inferred to 20 / 24 / 0. Q10 (coexistence with security.md) is the only question left open. Tests: Not run - docs only References: Refs #1242 --- THREAT_MODEL.md | 68 +++++++++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index a18d10fda..fbb2449d5 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -1,6 +1,6 @@ # Apache Pekko HTTP — Threat Model -**Status:** DRAFT — awaiting Pekko PMC review. Not yet ratified as a whole. **§14 Q1 (the DoS line) and Q2 (the CORS defaults) have been answered by a maintainer**, and §5b records the project's standing position on configuration defaults; these are settled model. The remaining questions in §14 are still open. +**Status:** Reviewed by a Pekko maintainer. **Q1-Q8 of §14 are answered** and are settled model, and §5b records the project's standing position on configuration defaults. **Q9 is resolved against the source** and **Q10 (coexistence with `security.md`) remains open**. No claim in this document is now uncited: every assertion is either cited to Pekko HTTP's own source and configuration, or stated by a maintainer. | | | | --- | --- | @@ -17,7 +17,7 @@ *(maintainer)* — stated by a Pekko maintainer in review of this document. *(inferred)* — reasoned from code or config defaults, **not yet confirmed**; each has a matching question in §14. -**Draft confidence:** 17 documented / 6 maintainer / 13 inferred — counting inline tags only. The §5a limits table and the §15 back-map carry a further ~35 documented facts under a single collective citation each, so the document is more evidence-backed than the bare ratio suggests. What is genuinely inferred now clusters in two places: the §3/§7/§9 non-goals and the negative claims in §5 — §14 Q7 and Q9 respectively. Four questions are closed: Q1 and Q2 answered by a maintainer, Q3 and Q5 resolved against the source (they remain listed so the PMC can confirm the *disposition*, not the fact). The DoS boundary, previously the largest inferred area, is now maintainer-settled. +**Confidence:** 20 documented / 24 maintainer / 0 inferred — counting inline tags only. The §5a limits table and the §15 back-map carry a further ~35 documented facts under a single collective citation each, so the document is more evidence-backed than the bare ratio suggests. **Nothing in the document is inferred any more.** Q1-Q2 and Q4-Q8 were answered by a maintainer in review; Q3, Q5 and Q9 were resolved against the source, the last of these replacing §5's negative claims with a cited scan. Q4 and Q5 are worth noting as corrections rather than confirmations — in both, the draft's proposed answer had the facts backwards and the code said otherwise, while the triage disposition it proposed survived intact. Apache Pekko HTTP is a Scala/Java toolkit for building HTTP-based services and clients on top of Pekko Streams. It provides a full HTTP/1.1 and HTTP/2 implementation — parsing, connection management, marshalling, and a routing DSL of composable "directives" — as an **embeddable library**, not a standalone server. The application supplies the routes, the authentication, and the deployment. @@ -47,17 +47,17 @@ Caller roles: | Benchmarks | `http-bench-jmh` | — | **no** — §3 | | Lint / build / docs | `http-scalafix`, `docs`, `project`, `scripts`, `legal` | — | **no** — §3 | -*(inferred — the in/out split is the ASF Security team's proposal; see §14 Q6)* +*(maintainer — the in/out split is confirmed by the Pekko maintainers; see §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`. *(inferred — §14 Q6)* +- **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. *(inferred — §14 Q7)* +- **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. *(inferred — §14 Q7)* +- **Attackers who already control the embedding process** are out of scope. *(maintainer — §14 Q7)* --- @@ -84,19 +84,21 @@ Read carefully, this makes a **graded** claim rather than a binary one: Pekko HT ## §5 Assumptions about the environment -- **Runtime.** A conformant JVM. Pekko HTTP does not defend against a hostile JVM or in-process attacker. *(inferred — §14 Q7)* +- **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`), but where a reverse proxy is used, termination is commonly the proxy's job. Cipher and protocol selection come from the JSSE context the application supplies. *(inferred — §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. `X-Forwarded-For` is not trusted or parsed into it automatically — deriving client IP from headers is the application's decision. *(inferred — §14 Q4)* +- **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`)* ### What Pekko HTTP does not do to its host -Negative claims, rarely written down and therefore high-priority confirmation targets *(all inferred — §14 Q9)*: +Negative claims, rarely written down and therefore verified against the source rather than asserted. The scan below covers the main sources of `http-core`, `http`, `parsing`, `http-caching` and `http-cors` *(documented — source scan, §14 Q9)*: -- Binds no port until the application calls a `bind*` method. -- Installs no signal handlers, spawns no child processes. -- Writes no files of its own accord; serves from disk only via directives the application installs (`getFromFile`, `getFromDirectory`). -- Does not mutate process-global state at initialization. +- **Binds no port until the application calls a `bind*` method.** Binding is reachable only through the public `Http().bind` / `bindAndHandle*` entry points (`Http.scala:179-292`); nothing binds at class or extension initialization. +- **Installs no signal handlers and spawns no child processes.** No `sun.misc.Signal`/`SignalHandler`, `ProcessBuilder` or `Runtime.exec` in the main sources — and, unlike `apache/pekko`, no `addShutdownHook` of its own. +- **Writes no files of its own accord** — no `FileOutputStream`, `Files.write`, `FileWriter` or `createTempFile`. It serves from disk only via directives the application installs (`getFromFile`, `getFromDirectory`), and those read. +- **Does not mutate process-global state at initialization** — no `System.setProperty`, `Security.setProperty`, `Security.addProvider` or `setDefault(...)`. + +**One inherited caveat.** These claims cover Pekko HTTP's own modules, not the `ActorSystem` it runs on. `apache/pekko` *does* register JVM shutdown hooks — one in `CoordinatedShutdown`, and a second in Artery when remoting is enabled — so an integrator will observe shutdown hooks in the process; they arrive with the actor system, and are modeled in the companion document, not here. --- @@ -173,7 +175,7 @@ Pekko HTTP therefore takes the following position *(maintainer)*: | --- | --- | --- | --- | | Any bound route | Request line (method, URI, version) | **Yes** | Pekko HTTP: §5a length limits | | Any bound route | Headers, incl. `Host`, `Cookie` | **Yes** | Pekko HTTP: count/length limits. App: semantic trust | -| Any bound route | `X-Forwarded-*` | **Yes** — trivially spoofable | **App/operator** — not validated by Pekko HTTP (§14 Q4) | +| 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 marshaller | Entity parsed to a domain type | **Yes** | Underlying JSON/XML library + app | @@ -188,9 +190,9 @@ 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. *(inferred — §14 Q7)* -- **A malicious upstream server**, where the application uses the client API against an untrusted endpoint. *(inferred — §14 Q7)* -- **A cross-origin web attacker**, where the application enables CORS. *(inferred — §14 Q2)* +- **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)* +- **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:** @@ -221,10 +223,10 @@ Pekko HTTP therefore takes the following position *(maintainer)*: - **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. *(inferred — §14 Q7)* -- **No CSRF protection.** No token issuance or verification is provided. *(inferred — §14 Q7)* -- **No output encoding / XSS defence.** Pekko HTTP renders what the application marshals. *(inferred — §14 Q7)* -- **No trusted client-IP derivation.** See §5 and §14 Q4. +- **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 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. ### False friends @@ -251,7 +253,7 @@ Pekko HTTP therefore takes the following position *(maintainer)*: 2. **Do not raise the §5a limits without understanding the memory cost** — each is multiplied by concurrent connections. 3. **If CORS is enabled, set `allowed-origins` explicitly.** Do not ship the `"*"` + `allow-credentials = yes` combination to a credentialed API (§5a). 4. **Compare credentials with `Credentials.verify`**, which is constant-time (§8 P8) — not with `==` on the secret, and not via `provideVerify` unless the supplied verifier is itself constant-time. -5. **Do not derive client identity from `X-Forwarded-For`** unless a trusted proxy sets it and the application validates the chain. +5. **Use `extractDirectClientIP`, not `extractClientIP`, for access control, rate limiting or audit logging.** The latter reads `X-Forwarded-For` / `X-Real-Ip`, which the client controls unless a trusted proxy overwrites them; it requires `remote-address-attribute = on` to fall back usefully. Derive identity from `X-Forwarded-For` only where a trusted proxy sets it and you validate the chain yourself. 6. **Validate and canonicalize any request-derived path** before passing it to a file-serving directive. 7. **Treat client-API responses from untrusted upstreams as untrusted input.** 8. **Consider `server-header = ""`** if product/version disclosure matters to your threat model. @@ -262,7 +264,7 @@ Pekko HTTP therefore takes the following position *(maintainer)*: - **Exposing a Pekko HTTP service directly to the internet** with no fronting proxy, contrary to the documented recommendation. - **Enabling `cors()` and leaving `allowed-origins = "*"`** on an API that uses cookies or bearer tokens. -- **Trusting `X-Forwarded-For`** for rate limiting, audit logging, or access control without a trusted-proxy chain. +- **Reaching for `extractClientIP`** — the more discoverable name — for rate limiting, audit logging or access control, where `extractDirectClientIP` is the one that cannot be chosen by the caller. - **Raising `max-content-length` to `infinite`** to accept large uploads, without a concurrency bound. - **Comparing credentials with `==`** inside an `authenticateBasic` verifier — or reaching for `provideVerify` with a non-constant-time verifier — instead of `Credentials.verify`. - **Passing a request path segment straight to `getFromFile`.** @@ -276,6 +278,7 @@ Pekko HTTP therefore takes the following position *(maintainer)*: - **"No authentication on routes."** Authentication is the application's responsibility (§9). A scan of this library cannot conclude a route is unauthenticated. - **"Request exceeding `max-uri-length` / `max-header-count` is rejected."** That is P1 working. - **"N concurrent connections / requests exhaust CPU, memory or sockets."** Volume-based resource exhaustion is `BY-DESIGN: property-disclaimed` per §14 Q1 — defence belongs to the fronting proxy (§10.1). Reports must show *one* in-limits request doing disproportionate work, not many requests doing proportionate work. A load-generator result is not a finding. +- **"`extractClientIP` trusts a client-supplied header."** By design and documented at the directive, with `extractDirectClientIP` provided as the trustworthy alternative (§14 Q4). `BY-DESIGN: property-disclaimed`. A report that `extractDirectClientIP` can be influenced by a header *is* in scope. - **"CORS allows any origin."** Reflects the shipped default and requires the application to have opted into `cors()`. A request to change the default is `BY-DESIGN: default-configuration` per §5b; a misconfigured deployment is a finding against the *application*, not the library. - **"`allow-credentials = yes` with `allowed-origins = "*"` sends `Access-Control-Allow-Origin: *` with credentials."** It does not — the literal `*` is sent only when `allowCredentials` is false, otherwise the request `Origin` is echoed (`CorsSettingsImpl.scala:64`, covered by `CorsDirectivesSpec`). Reports asserting the literal `*`-with-credentials combination are factually wrong. - **"Credential comparison is vulnerable to a timing attack."** Check which comparator the report exercises: `Credentials.verify` is constant-time (§8 P8), so the claim is wrong against it; against an application's own `provideVerify` comparator it is a finding in that application, not this library. @@ -329,17 +332,21 @@ The operative test is *content vs. volume*: one well-formed, in-limits request d **Q3 — File-serving directives.** *Resolved from code — confirm the disposition only.* `safeDirectoryChildPath` contains traversal by two stated measures: a path segment must not be `..` and must not contain `/` or `\\`; and the resolved file's `File.getCanonicalPath` must be prefixed by the base path's. So containment **is** claimed, and a genuine escape from the configured root is `VALID`; passing an unvalidated path in is a §11 misuse. One residual the code comment itself flags: containment rests on `getCanonicalPath`, whose symlink resolution is platform-dependent — *is a symlink out of the served root a `VALID` finding, or an operator responsibility?* *(documented — `FileAndResourceDirectives.scala:229-274`)* -**Q4 — `X-Forwarded-For` and client identity.** *Proposed:* Pekko HTTP neither parses nor trusts forwarding headers; `remote-address-attribute` is strictly the socket peer, and deriving client IP is entirely the application's job — so "forwarding header is spoofable" is `BY-DESIGN: property-disclaimed`. Confirm? +**Q4 — `X-Forwarded-For` and client identity. ANSWERED *(maintainer)*.** The disposition is confirmed, but the draft's stated basis for it was wrong and is corrected here. Pekko HTTP **does** parse forwarding headers: `extractClientIP` resolves `X-Forwarded-For` (first address) → `X-Real-Ip` → the `remoteAddress` attribute, in that order (`MiscDirectives.scala:142-145`). What is true is that it never does so *implicitly* — `remote-address-attribute` populates the attribute from the socket peer only, and a route gets header-derived values solely because it called `extractClientIP`. + +**Answer:** the spoofability is documented at the directive rather than defended against. `extractClientIP`'s own scaladoc warns that *"the headers are under the control of the client unless a trusted proxy in front of this server overwrites them"* and directs the reader to `extractDirectClientIP` *"where the address must not be chosen by the client, for example for access control or rate limiting"* — a directive added for exactly this purpose (#1219, `@since 2.0.0`), which reads the attribute alone and ignores headers. So "`extractClientIP` trusts a spoofable header" is `BY-DESIGN: property-disclaimed`: the library offers both a convenient and a trustworthy accessor, documents which is which, and leaves the choice to the application. A defect in `extractDirectClientIP` — anything client-controlled reaching it — would be `VALID`. *(documented — `MiscDirectives.scala:39-64`)* + +**Q5 — Constant-time credential comparison. ANSWERED *(maintainer)*.** *Resolved from code — this document's earlier draft had it backwards.* `Credentials.Provided.verify` does compare, via `secure_==` (`EnhancedByteArray.scala:37`), which is constant-time; the library therefore **does** provide the guarantee, recorded as §8 P8. It is conditional on the verifier calling `verify` — `provideVerify` hands the raw secret to application code and waives it. **Answer *(maintainer)*:** the split is confirmed. A timing finding against `verify` — anything that makes the comparison data-dependent — is `VALID` at §8 P8's severity. One against an application's own `provideVerify` comparator is `BY-DESIGN: property-disclaimed`: passing the raw secret to application code waives the guarantee by construction, and §10.4 states the responsibility. -**Q5 — Constant-time credential comparison.** *Resolved from code — this document's earlier draft had it backwards.* `Credentials.Provided.verify` does compare, via `secure_==` (`EnhancedByteArray.scala:37`), which is constant-time; the library therefore **does** provide the guarantee, recorded as §8 P8. It is conditional on the verifier calling `verify` — `provideVerify` hands the raw secret to application code and waives it. *Proposed:* a timing finding against `verify` is `VALID`; one against an application's own `provideVerify` comparator is `BY-DESIGN: property-disclaimed`. Confirm the split? +**Q6 — Module in/out split (§2 table). ANSWERED *(maintainer)*.** **Answer:** the split shown in §2 is confirmed as the maintainers' own, not merely the ASF Security team's proposal. `http-caching` is **in** model — cache-key confusion is a real class and the directive ships as part of the supported surface. `http-scalafix` is correctly **out**, along with the test kits, `http-bench-jmh`, `docs`, `project`, `scripts` and `legal`: a finding in any of them is `OUT-OF-MODEL: unsupported-component` per §3. -**Q6 — Module in/out split (§2 table).** *Proposed:* the split shown. Specifically: should `http-caching` be in model (cache-key confusion is a real class), and is `http-scalafix` correctly out? +**Q7 — The §3/§7/§9 non-goals. ANSWERED *(maintainer)*.** **Answer:** the split is confirmed as stated. Pekko HTTP provides **no** authentication system, authorization policy, CSRF protection or XSS/output encoding — reports against those are `BY-DESIGN: property-disclaimed` per §9. **Out** of the adversary model: attackers with code execution in the embedding JVM, and a malicious embedding application (a route that deliberately leaks is an application bug). **In**: the remote HTTP client (primary), a malicious upstream server where the application drives the client API against an untrusted endpoint, and a cross-origin web attacker where the application has enabled `cors()`. This closes the §3, §7 and §9 non-goals as maintainer-stated rather than inferred. -**Q7 — The §3/§7/§9 non-goals.** *Proposed:* Pekko HTTP provides no authentication system, no authorization policy, no CSRF protection and no XSS/output encoding, and in-JVM attackers plus a malicious embedding application are out of the adversary model — while the remote HTTP client, a malicious upstream (client API), and a cross-origin web attacker (where CORS is on) are all **in**. Confirm the split? +**Q8 — TLS. ANSWERED *(maintainer)*.** **Answer:** confirmed. Where Pekko HTTP terminates TLS via `HttpsConnectionContext`, protocol and cipher selection come from the JSSE context the application supplies; Pekko HTTP neither pins a cipher suite nor overrides the JDK's defaults. "Weak cipher accepted" is therefore a deployment finding against that context or the JDK, not a library one — `OUT-OF-MODEL: trusted-input` per §6, which marks configuration as operator-supplied. A defect in how Pekko HTTP *drives* the context — failing to apply a supplied restriction, or continuing after a handshake failure — would be `VALID`. -**Q8 — TLS.** *Proposed:* where Pekko HTTP terminates TLS, protocol and cipher selection come from the application-supplied JSSE context, so "weak cipher accepted" is a deployment finding, not a library one. Confirm — and is in-process termination a supported production posture, or is proxy termination the expectation? +On the second half: **in-process termination is supported**, and `HttpsConnectionContext` is a first-class API. It is not, however, the posture the documentation steers production deployments toward — §4's quoted recommendation to front the service with an enterprise-grade routing solution or load balancer applies to TLS as much as to volume defence (§14 Q1), and in such a deployment termination is commonly the proxy's job. Both are supported; the fronted one is what the docs recommend. -**Q9 — The negative claims in §5.** These are inferred and hard to cite. Are any wrong — does Pekko HTTP bind ports, write files, or mutate process-global state in ways an integrator would not expect? +**Q9 — The negative claims in §5.** *Resolved from code — confirm the disposition only.* Scanned the main sources of `http-core`, `http`, `parsing`, `http-caching` and `http-cors`: no `addShutdownHook`, no `ProcessBuilder`/`Runtime.exec`, no `Signal`/`SignalHandler`, no file-writing API, no `System.setProperty`/`Security.*`/`setDefault`, and no bind outside the public `Http().bind*` entry points. All four claims hold **for Pekko HTTP's own code**, and §5 now cites the scan rather than asserting them. The caveat worth a maintainer's eye is the inherited one: the `ActorSystem` registers shutdown hooks that Pekko HTTP does not, so *"Pekko HTTP installs no shutdown hook"* is true while *"a Pekko HTTP process has no shutdown hook"* is false. **Is stating that boundary here — rather than deferring the whole topic to the companion model — the split you want?** **Q10 — Coexistence (meta).** `docs/src/main/paradox/security.md` has a "Security model" section that this document expands considerably. *Proposed:* this file becomes canonical for **scope and triage**, `security.md` stays canonical for **announcements and reporting**, and its "Security model" section becomes a short pointer here. Agree? @@ -363,5 +370,6 @@ The operative test is *content vs. volume*: one well-formed, in-limits request d | `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 a canonical-path prefix | `FileAndResourceDirectives.scala` | §9, §14 Q3 | | `remote-address-attribute = off` | `http-core/reference.conf` | §5, §9, §14 Q4 | +| `extractClientIP` reads `X-Forwarded-For`/`X-Real-Ip`; `extractDirectClientIP` reads the attribute alone | `MiscDirectives.scala` | §5, §6, §9, §10.5, §11a, §14 Q4 | | CORS: `*` + credentials echoes the request `Origin` | `http-cors/reference.conf` | §5a, §9, §11a, §14 Q2 | | `http-cors` code donated by Lomig Mégard, defaults inherited with it | `legal/CorsNotice.txt`, `NOTICE` | §5b, §14 Q2 | From 04599049ddabefde57f5aae0375a21015dc55037 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Sun, 30 Aug 2026 14:45:48 +0100 Subject: [PATCH 5/9] Answer Q9: Pekko HTTP registers no shutdown hook of its own MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation: Q9 was resolved against the source but left the disposition open: §5's scan showed Pekko HTTP adds no JVM shutdown hook, while the ActorSystem it runs on does. Whether to state that boundary here or defer the topic to the companion model needed a ruling, and the distinction is easy to misattribute in triage. Modification: - Q9 answered: highlight the ActorSystem's hooks in §5 so integrators are not surprised, but the claim this document makes is that Pekko HTTP registers none of its own. - Promoted "Registers no JVM shutdown hook" from a trailing clause on the signal-handler bullet to its own §5 claim, so the load-bearing statement is stated directly rather than implied. - Sharpened the §5 caveat to separate the two readings: "a Pekko HTTP process has no shutdown hook" is false, "Pekko HTTP registers no shutdown hook" is true and is what this section asserts. - New §11a non-finding: a report that the process registers shutdown hooks is OUT-OF-MODEL: unsupported-component, since the hooks are CoordinatedShutdown's and, with remoting enabled, Artery's. Result: Q10 (coexistence with security.md) is the only question left open. Provenance is 20 documented / 26 maintainer / 0 inferred. Tests: Not run - docs only References: Refs #1242 --- THREAT_MODEL.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index fbb2449d5..3e2fdf3f6 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -1,6 +1,6 @@ # Apache Pekko HTTP — Threat Model -**Status:** Reviewed by a Pekko maintainer. **Q1-Q8 of §14 are answered** and are settled model, and §5b records the project's standing position on configuration defaults. **Q9 is resolved against the source** and **Q10 (coexistence with `security.md`) remains open**. No claim in this document is now uncited: every assertion is either cited to Pekko HTTP's own source and configuration, or stated by a maintainer. +**Status:** Reviewed by a Pekko maintainer. **Q1-Q8 of §14 are answered** and are settled model, and §5b records the project's standing position on configuration defaults. **Q9 is resolved against the source and ruled on**, leaving **Q10 (coexistence with `security.md`) as the only open question**. No claim in this document is now uncited: every assertion is either cited to Pekko HTTP's own source and configuration, or stated by a maintainer. | | | | --- | --- | @@ -17,7 +17,7 @@ *(maintainer)* — stated by a Pekko maintainer in review of this document. *(inferred)* — reasoned from code or config defaults, **not yet confirmed**; each has a matching question in §14. -**Confidence:** 20 documented / 24 maintainer / 0 inferred — counting inline tags only. The §5a limits table and the §15 back-map carry a further ~35 documented facts under a single collective citation each, so the document is more evidence-backed than the bare ratio suggests. **Nothing in the document is inferred any more.** Q1-Q2 and Q4-Q8 were answered by a maintainer in review; Q3, Q5 and Q9 were resolved against the source, the last of these replacing §5's negative claims with a cited scan. Q4 and Q5 are worth noting as corrections rather than confirmations — in both, the draft's proposed answer had the facts backwards and the code said otherwise, while the triage disposition it proposed survived intact. +**Confidence:** 20 documented / 26 maintainer / 0 inferred — counting inline tags only. The §5a limits table and the §15 back-map carry a further ~35 documented facts under a single collective citation each, so the document is more evidence-backed than the bare ratio suggests. **Nothing in the document is inferred any more.** Q1-Q2 and Q4-Q9 were answered by a maintainer in review; Q3, Q5 and Q9 were resolved against the source first, the last of these replacing §5's negative claims with a cited scan. Q4 and Q5 are worth noting as corrections rather than confirmations — in both, the draft's proposed answer had the facts backwards and the code said otherwise, while the triage disposition it proposed survived intact. Apache Pekko HTTP is a Scala/Java toolkit for building HTTP-based services and clients on top of Pekko Streams. It provides a full HTTP/1.1 and HTTP/2 implementation — parsing, connection management, marshalling, and a routing DSL of composable "directives" — as an **embeddable library**, not a standalone server. The application supplies the routes, the authentication, and the deployment. @@ -94,11 +94,12 @@ Read carefully, this makes a **graded** claim rather than a binary one: Pekko HT Negative claims, rarely written down and therefore verified against the source rather than asserted. The scan below covers the main sources of `http-core`, `http`, `parsing`, `http-caching` and `http-cors` *(documented — source scan, §14 Q9)*: - **Binds no port until the application calls a `bind*` method.** Binding is reachable only through the public `Http().bind` / `bindAndHandle*` entry points (`Http.scala:179-292`); nothing binds at class or extension initialization. -- **Installs no signal handlers and spawns no child processes.** No `sun.misc.Signal`/`SignalHandler`, `ProcessBuilder` or `Runtime.exec` in the main sources — and, unlike `apache/pekko`, no `addShutdownHook` of its own. +- **Installs no signal handlers and spawns no child processes.** No `sun.misc.Signal`/`SignalHandler`, `ProcessBuilder` or `Runtime.exec` in the main sources. +- **Registers no JVM shutdown hook.** There is no `addShutdownHook` anywhere in Pekko HTTP's main sources. Any hook an integrator observes comes from the actor system, not from here — see the caveat below. - **Writes no files of its own accord** — no `FileOutputStream`, `Files.write`, `FileWriter` or `createTempFile`. It serves from disk only via directives the application installs (`getFromFile`, `getFromDirectory`), and those read. - **Does not mutate process-global state at initialization** — no `System.setProperty`, `Security.setProperty`, `Security.addProvider` or `setDefault(...)`. -**One inherited caveat.** These claims cover Pekko HTTP's own modules, not the `ActorSystem` it runs on. `apache/pekko` *does* register JVM shutdown hooks — one in `CoordinatedShutdown`, and a second in Artery when remoting is enabled — so an integrator will observe shutdown hooks in the process; they arrive with the actor system, and are modeled in the companion document, not here. +**One inherited caveat, highlighted because it is easy to misattribute.** The claims above cover Pekko HTTP's own modules, not the `ActorSystem` it runs on. `apache/pekko` *does* register JVM shutdown hooks — one in `CoordinatedShutdown`, and a second in Artery when remoting is enabled — so an integrator running a Pekko HTTP service **will** observe shutdown hooks in the process. They arrive with the actor system and are modeled in the companion document. **Pekko HTTP itself adds none**, and that is the claim this section makes: *"a Pekko HTTP process has no shutdown hook"* is false, while *"Pekko HTTP registers no shutdown hook"* is true and is what §14 Q9 settles. *(maintainer — §14 Q9)* --- @@ -283,6 +284,7 @@ Pekko HTTP therefore takes the following position *(maintainer)*: - **"`allow-credentials = yes` with `allowed-origins = "*"` sends `Access-Control-Allow-Origin: *` with credentials."** It does not — the literal `*` is sent only when `allowCredentials` is false, otherwise the request `Origin` is echoed (`CorsSettingsImpl.scala:64`, covered by `CorsDirectivesSpec`). Reports asserting the literal `*`-with-credentials combination are factually wrong. - **"Credential comparison is vulnerable to a timing attack."** Check which comparator the report exercises: `Credentials.verify` is constant-time (§8 P8), so the claim is wrong against it; against an application's own `provideVerify` comparator it is a finding in that application, not this library. - **Findings in `*-tests`, `http-testkit*`, `http-bench-jmh`, `http-scalafix`, `docs`** — `OUT-OF-MODEL: unsupported-component` per §3. +- **"The process registers JVM shutdown hooks."** Pekko HTTP registers none (§5, §14 Q9). The hooks are `CoordinatedShutdown`'s and, with remoting enabled, Artery's — they belong to `apache/pekko`'s model. `OUT-OF-MODEL: unsupported-component`. - **Findings in the actor or stream layer** — belongs to `apache/pekko`'s model, not this one. --- @@ -346,7 +348,9 @@ The operative test is *content vs. volume*: one well-formed, in-limits request d On the second half: **in-process termination is supported**, and `HttpsConnectionContext` is a first-class API. It is not, however, the posture the documentation steers production deployments toward — §4's quoted recommendation to front the service with an enterprise-grade routing solution or load balancer applies to TLS as much as to volume defence (§14 Q1), and in such a deployment termination is commonly the proxy's job. Both are supported; the fronted one is what the docs recommend. -**Q9 — The negative claims in §5.** *Resolved from code — confirm the disposition only.* Scanned the main sources of `http-core`, `http`, `parsing`, `http-caching` and `http-cors`: no `addShutdownHook`, no `ProcessBuilder`/`Runtime.exec`, no `Signal`/`SignalHandler`, no file-writing API, no `System.setProperty`/`Security.*`/`setDefault`, and no bind outside the public `Http().bind*` entry points. All four claims hold **for Pekko HTTP's own code**, and §5 now cites the scan rather than asserting them. The caveat worth a maintainer's eye is the inherited one: the `ActorSystem` registers shutdown hooks that Pekko HTTP does not, so *"Pekko HTTP installs no shutdown hook"* is true while *"a Pekko HTTP process has no shutdown hook"* is false. **Is stating that boundary here — rather than deferring the whole topic to the companion model — the split you want?** +**Q9 — The negative claims in §5. ANSWERED *(maintainer)*.** *Resolved from code, then ruled on.* Scanned the main sources of `http-core`, `http`, `parsing`, `http-caching` and `http-cors`: no `addShutdownHook`, no `ProcessBuilder`/`Runtime.exec`, no `Signal`/`SignalHandler`, no file-writing API, no `System.setProperty`/`Security.*`/`setDefault`, and no bind outside the public `Http().bind*` entry points. All claims hold, and §5 cites the scan rather than asserting them. + +**Answer:** state the boundary here rather than deferring it. The `ActorSystem`'s shutdown hooks are worth **highlighting** in §5 so an integrator is not surprised by them, but the claim this document makes and stands behind is that **Pekko HTTP registers none of its own**. A report that Pekko HTTP installs a shutdown hook is factually wrong (§11a); one about the hooks `CoordinatedShutdown` or Artery register belongs to `apache/pekko`'s model, and is `OUT-OF-MODEL: unsupported-component` here. **Q10 — Coexistence (meta).** `docs/src/main/paradox/security.md` has a "Security model" section that this document expands considerably. *Proposed:* this file becomes canonical for **scope and triage**, `security.md` stays canonical for **announcements and reporting**, and its "Security model" section becomes a short pointer here. Agree? From 57dbfaf9c8cb5829d147fbbab01d0b78f5799bb7 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Sun, 30 Aug 2026 14:48:15 +0100 Subject: [PATCH 6/9] Answer Q10: three documents, each canonical for one thing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation: Q10 was the last open question: which document is canonical for what, now that THREAT_MODEL.md, SECURITY.md and docs/src/main/paradox/security.md all carry security information. apache/pekko#3478 settled the same question for the companion project, and following its split keeps the two models consistent for anyone triaging across both. Modification: - Q10 answered with the three-document table from apache/pekko#3478: SECURITY.md is canonical for the reporting policy, THREAT_MODEL.md for scope and triage, and docs/src/main/paradox/security.md for announcements and the docs-site index. Every other document links rather than restates. - docs/src/main/paradox/security.md gains links to SECURITY.md and THREAT_MODEL.md, and its reporting paragraph now names security@apache.org rather than "our private security mailing list", which does not exist. This mirrors the equivalent change in apache/pekko#3478. - Corrected the draft's own proposal, which suggested reducing the "Security model" section of security.md to a pointer. It is kept: §4 quotes it as the documented source of the "should not be exposed to the public internet directly" posture and §15 back-maps four claims to it, so reducing it would delete the evidence this model is built on. Result: All ten §14 questions are answered. One item is referred rather than settled, as the ruling requires: security.md carries an upstream-coordination sentence about sharing reports with the Lightbend Akka team that SECURITY.md does not, and a reporting statement is promoted verbatim or dropped by maintainer decision, never silently moved. Provenance is 20 documented / 27 maintainer / 0 inferred. Tests: Not run - docs only References: Refs #1242 --- THREAT_MODEL.md | 18 +++++++++++++++--- docs/src/main/paradox/security.md | 6 +++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index 3e2fdf3f6..e6dffd310 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -1,6 +1,6 @@ # Apache Pekko HTTP — Threat Model -**Status:** Reviewed by a Pekko maintainer. **Q1-Q8 of §14 are answered** and are settled model, and §5b records the project's standing position on configuration defaults. **Q9 is resolved against the source and ruled on**, leaving **Q10 (coexistence with `security.md`) as the only open question**. No claim in this document is now uncited: every assertion is either cited to Pekko HTTP's own source and configuration, or stated by a maintainer. +**Status:** Reviewed by a Pekko maintainer. **Q1-Q8 of §14 are answered** and are settled model, and §5b records the project's standing position on configuration defaults. **All ten questions in §14 are answered.** One item is referred rather than settled: whether the upstream-coordination sentence in `security.md` should be promoted into `SECURITY.md` (§14 Q10). No claim in this document is now uncited: every assertion is either cited to Pekko HTTP's own source and configuration, or stated by a maintainer. | | | | --- | --- | @@ -17,7 +17,7 @@ *(maintainer)* — stated by a Pekko maintainer in review of this document. *(inferred)* — reasoned from code or config defaults, **not yet confirmed**; each has a matching question in §14. -**Confidence:** 20 documented / 26 maintainer / 0 inferred — counting inline tags only. The §5a limits table and the §15 back-map carry a further ~35 documented facts under a single collective citation each, so the document is more evidence-backed than the bare ratio suggests. **Nothing in the document is inferred any more.** Q1-Q2 and Q4-Q9 were answered by a maintainer in review; Q3, Q5 and Q9 were resolved against the source first, the last of these replacing §5's negative claims with a cited scan. Q4 and Q5 are worth noting as corrections rather than confirmations — in both, the draft's proposed answer had the facts backwards and the code said otherwise, while the triage disposition it proposed survived intact. +**Confidence:** 20 documented / 27 maintainer / 0 inferred — counting inline tags only. The §5a limits table and the §15 back-map carry a further ~35 documented facts under a single collective citation each, so the document is more evidence-backed than the bare ratio suggests. **Nothing in the document is inferred any more.** Q1-Q2 and Q4-Q9 were answered by a maintainer in review; Q3, Q5 and Q9 were resolved against the source first, the last of these replacing §5's negative claims with a cited scan. Q4 and Q5 are worth noting as corrections rather than confirmations — in both, the draft's proposed answer had the facts backwards and the code said otherwise, while the triage disposition it proposed survived intact. Apache Pekko HTTP is a Scala/Java toolkit for building HTTP-based services and clients on top of Pekko Streams. It provides a full HTTP/1.1 and HTTP/2 implementation — parsing, connection management, marshalling, and a routing DSL of composable "directives" — as an **embeddable library**, not a standalone server. The application supplies the routes, the authentication, and the deployment. @@ -352,7 +352,19 @@ On the second half: **in-process termination is supported**, and `HttpsConnectio **Answer:** state the boundary here rather than deferring it. The `ActorSystem`'s shutdown hooks are worth **highlighting** in §5 so an integrator is not surprised by them, but the claim this document makes and stands behind is that **Pekko HTTP registers none of its own**. A report that Pekko HTTP installs a shutdown hook is factually wrong (§11a); one about the hooks `CoordinatedShutdown` or Artery register belongs to `apache/pekko`'s model, and is `OUT-OF-MODEL: unsupported-component` here. -**Q10 — Coexistence (meta).** `docs/src/main/paradox/security.md` has a "Security model" section that this document expands considerably. *Proposed:* this file becomes canonical for **scope and triage**, `security.md` stays canonical for **announcements and reporting**, and its "Security model" section becomes a short pointer here. Agree? +**Q10 — Coexistence. ANSWERED *(maintainer)*.** Following the split adopted in [`apache/pekko#3478`](https://github.com/apache/pekko/pull/3478). Three documents carry security information, each canonical for one thing: + +| Document | Canonical for | Reached by | +| --- | --- | --- | +| [`SECURITY.md`](SECURITY.md) | **The reporting policy.** The strongest and canonical statement of how to report and what the project undertakes | Anyone arriving via the repository, and every other document | +| `THREAT_MODEL.md` (this document) | **Scope** — what is and is not a vulnerability, and how a report is triaged | Reporters, triagers, scanning tools | +| `docs/src/main/paradox/security.md` | Security announcements, and the documentation-site index of security material | Readers of the documentation site | + +Every other document **links** to `SECURITY.md` for the reporting policy and to this document for scope, rather than restating either. A change to the policy is therefore made in one place. + +**Note the correction to the draft's proposal.** It suggested that `security.md`'s "Security model" section be reduced to a pointer here. It is *not*, and must not be: §4 quotes that section verbatim as the documented source of the "should not be exposed to the public internet directly" posture, and §15 back-maps four separate claims to it. Reducing it to a pointer would delete the evidence this document is built on and leave §4 citing a redirect. The section stays; `security.md` gains links to `SECURITY.md` and to this file, matching the minimal change made in `apache/pekko#3478`. + +**One statement is referred, not migrated.** `security.md` carries an upstream-coordination sentence that `SECURITY.md` does not: *"Ideally, any issues affecting Apache Pekko and Akka should be reported to Apache team first. We will share the report with the Lightbend Akka team."* Per the same ruling, a reporting statement living outside `SECURITY.md` is referred to the maintainers for a decision — promoted verbatim or dropped, never silently moved. It is left untouched in `security.md` pending that decision. (`apache/pekko` resolved its equivalent by carrying a general sentence in `SECURITY.md`: *"The Pekko PMC will coordinate responsible disclosure with affected upstream maintainers where needed."*) --- diff --git a/docs/src/main/paradox/security.md b/docs/src/main/paradox/security.md index 364c0a977..043e121f5 100644 --- a/docs/src/main/paradox/security.md +++ b/docs/src/main/paradox/security.md @@ -15,7 +15,9 @@ This mailing list also has announcements of releases for Apache projects. ## Reporting Vulnerabilities -We strongly encourage people to report such problems to our private security mailing list first, before disclosing them in a public forum. +We strongly encourage people to report such problems privately to the Apache Security team at +`security@apache.org` first, before disclosing them in a public forum. Apache Pekko does not +operate a separate project security list. Please follow the [guidelines](https://www.apache.org/security/) laid down by the Apache Security team. @@ -25,3 +27,5 @@ report with the Lightbend Akka team. ## References * [Akka HTTP security fixes](https://doc.akka.io/docs/akka-http/10.2/security.html) + * [Security Policy](https://github.com/apache/pekko-http/blob/main/SECURITY.md) + * [Threat Model](https://github.com/apache/pekko-http/blob/main/THREAT_MODEL.md) From 17cbbf6bc7edc199fa786a03cbfc40443c829c12 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Mon, 31 Aug 2026 13:43:43 +0100 Subject: [PATCH 7/9] =?UTF-8?q?Correct=20two=20=C2=A75/=C2=A79=20claims=20?= =?UTF-8?q?against=20the=20source=20and=20answer=20Q3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation: Maintainer review found two claims that do not hold on main and one internal inconsistency: - §5 claimed Pekko HTTP writes no files, but fileUploadAll creates temp files and storeUploadedFile(s) writes entity bytes to an application-chosen destination (FileUploadDirectives.scala:178). - §9 and §14 Q3 described safeDirectoryChildPath as containing traversal, but its canonical-path check compares strings, so a symlink resolving into a sibling directory that shares the served root as a string prefix escapes it (fix in flight in #1218). - The status line said both "Q1-Q8 answered" and "all ten answered" while Q3 still ended in an open question, and the pinned commit was the PR's own first commit rather than the main commit reviewed. Modification: Restate the §5 file-system claim with the upload-directive carve-out and correct Q9 accordingly. Answer Q3 as a correction: the escape is VALID under §5b.4 and fixed by #1218; update §9 and the §15 back-map to match. Note in §12 that #1217 would invalidate the shutdown-hook claim on merge. Fix the status line, the tag tally, and the commit pin (444d939 -> 85d7243, the main commit the branch is based on). Result: Every §5 negative claim matches the source at the pinned commit, Q3 is answered consistently with the "all ten answered" status, and the two in-flight PRs that touch the model's claims (#1217, #1218) are cross-linked. Tests: Not run - docs only References: Refs #1218, Refs #1217 --- THREAT_MODEL.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index e6dffd310..762c9dbed 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -1,11 +1,11 @@ # Apache Pekko HTTP — Threat Model -**Status:** Reviewed by a Pekko maintainer. **Q1-Q8 of §14 are answered** and are settled model, and §5b records the project's standing position on configuration defaults. **All ten questions in §14 are answered.** One item is referred rather than settled: whether the upstream-coordination sentence in `security.md` should be promoted into `SECURITY.md` (§14 Q10). No claim in this document is now uncited: every assertion is either cited to Pekko HTTP's own source and configuration, or stated by a maintainer. +**Status:** Reviewed by a Pekko maintainer. **All ten questions in §14 are answered** and are settled model, and §5b records the project's standing position on configuration defaults. One item is referred rather than settled: whether the upstream-coordination sentence in `security.md` should be promoted into `SECURITY.md` (§14 Q10). No claim in this document is now uncited: every assertion is either cited to Pekko HTTP's own source and configuration, or stated by a maintainer. | | | | --- | --- | | **Project** | Apache Pekko HTTP | -| **Written against** | commit `444d939`, `main` | +| **Written against** | commit `85d7243`, `main` | | **Date** | 2026-08-27 | | **Authors** | ASF Security team, at the request of the Pekko PMC | | **Version binding** | Versioned alongside the project. A report against version *N* is triaged against the model as it stood at *N*, not at `main`. | @@ -17,7 +17,7 @@ *(maintainer)* — stated by a Pekko maintainer in review of this document. *(inferred)* — reasoned from code or config defaults, **not yet confirmed**; each has a matching question in §14. -**Confidence:** 20 documented / 27 maintainer / 0 inferred — counting inline tags only. The §5a limits table and the §15 back-map carry a further ~35 documented facts under a single collective citation each, so the document is more evidence-backed than the bare ratio suggests. **Nothing in the document is inferred any more.** Q1-Q2 and Q4-Q9 were answered by a maintainer in review; Q3, Q5 and Q9 were resolved against the source first, the last of these replacing §5's negative claims with a cited scan. Q4 and Q5 are worth noting as corrections rather than confirmations — in both, the draft's proposed answer had the facts backwards and the code said otherwise, while the triage disposition it proposed survived intact. +**Confidence:** 20 documented / 28 maintainer / 0 inferred — counting inline tags only. The §5a limits table and the §15 back-map carry a further ~35 documented facts under a single collective citation each, so the document is more evidence-backed than the bare ratio suggests. **Nothing in the document is inferred any more.** Q1-Q9 were answered by a maintainer in review; Q3, Q5 and Q9 were resolved against the source first, the last of these replacing §5's negative claims with a cited scan. Q3, Q4, Q5 and (in part) Q9 are worth noting as corrections rather than confirmations — in each, the draft's claim did not survive contact with the code: Q4 and Q5 had the facts backwards while the proposed triage disposition survived intact; Q3's review found a real defect in the containment check it examined (fixed in [#1218](https://github.com/apache/pekko-http/pull/1218)); and Q9's source scan had missed the one place the main sources write files (§5). Apache Pekko HTTP is a Scala/Java toolkit for building HTTP-based services and clients on top of Pekko Streams. It provides a full HTTP/1.1 and HTTP/2 implementation — parsing, connection management, marshalling, and a routing DSL of composable "directives" — as an **embeddable library**, not a standalone server. The application supplies the routes, the authentication, and the deployment. @@ -96,7 +96,7 @@ Negative claims, rarely written down and therefore verified against the source r - **Binds no port until the application calls a `bind*` method.** Binding is reachable only through the public `Http().bind` / `bindAndHandle*` entry points (`Http.scala:179-292`); nothing binds at class or extension initialization. - **Installs no signal handlers and spawns no child processes.** No `sun.misc.Signal`/`SignalHandler`, `ProcessBuilder` or `Runtime.exec` in the main sources. - **Registers no JVM shutdown hook.** There is no `addShutdownHook` anywhere in Pekko HTTP's main sources. Any hook an integrator observes comes from the actor system, not from here — see the caveat below. -- **Writes no files of its own accord** — no `FileOutputStream`, `Files.write`, `FileWriter` or `createTempFile`. It serves from disk only via directives the application installs (`getFromFile`, `getFromDirectory`), and those read. +- **Touches the file system only through directives the application installs.** The disk-serving directives (`getFromFile`, `getFromDirectory`) read. The upload directives **write**: `storeUploadedFile(s)` streams entity bytes to a destination the application's function chooses, and `fileUploadAll` buffers each part into a `Files.createTempFile` temp file registered with `deleteOnExit` (`FileUploadDirectives.scala:178-179`). Both are reachable only where the application binds them to a route; nothing writes at initialization or outside an installed directive. Beyond `FileUploadDirectives`, the main sources contain no `FileOutputStream`, `Files.write`, `FileWriter` or `createTempFile`. *(The draft claimed Pekko HTTP writes no files at all; the scan missed the upload directives — corrected in review, §14 Q9.)* - **Does not mutate process-global state at initialization** — no `System.setProperty`, `Security.setProperty`, `Security.addProvider` or `setDefault(...)`. **One inherited caveat, highlighted because it is easy to misattribute.** The claims above cover Pekko HTTP's own modules, not the `ActorSystem` it runs on. `apache/pekko` *does* register JVM shutdown hooks — one in `CoordinatedShutdown`, and a second in Artery when remoting is enabled — so an integrator running a Pekko HTTP service **will** observe shutdown hooks in the process. They arrive with the actor system and are modeled in the companion document. **Pekko HTTP itself adds none**, and that is the claim this section makes: *"a Pekko HTTP process has no shutdown hook"* is false, while *"Pekko HTTP registers no shutdown hook"* is true and is what §14 Q9 settles. *(maintainer — §14 Q9)* @@ -243,7 +243,7 @@ Pekko HTTP therefore takes the following position *(maintainer)*: - **Slow-loris and connection exhaustion** — `idle-timeout` and `max-connections` (P5) bound what one connection holds and how many are accepted, but exhausting those bounds by volume is disclaimed (§14 Q1). A single connection that evades `idle-timeout` while holding resources is the in-scope version. - **Decompression bombs** in request bodies, where the application enables decoding. - **SSRF** via the client API, where the application takes a URL from a request. -- **Path traversal** in file-serving directives is *not* left to the caller — `safeDirectoryChildPath` contains it (§14 Q3). What remains the caller's is the surrounding choice: which root is served, and whether a symlink may point out of it. +- **Path traversal** in file-serving directives is *not* left to the caller — `safeDirectoryChildPath` claims containment, and a genuine escape from the configured root is `VALID` per §5b.4. Review of this model found exactly such a defect: the containment check compared canonical paths as **strings**, so a symbolic link resolving into a sibling directory whose name shares the served root as a string prefix (`/var/www` vs `/var/www-private`) escaped it — fixed by [#1218](https://github.com/apache/pekko-http/pull/1218), which compares path elements (§14 Q3). What remains the caller's is the surrounding choice: which root is served, and whether the tree under it contains symlinks at all. - **XXE** in XML marshallers — a property of the underlying parser. --- @@ -296,6 +296,7 @@ Pekko HTTP therefore takes the following position *(maintainer)*: - Taking on any authentication, authorization, or rate-limiting responsibility currently disclaimed in §9. - A change to the documented "do not expose directly" posture in `security.md`. - Promotion of a §3 module into the supported surface. +- A change to the §5 process-behaviour claims — for example, [#1217](https://github.com/apache/pekko-http/pull/1217) proposes replacing the per-upload `deleteOnExit` with a single JVM shutdown hook registered by Pekko HTTP itself, which on merge invalidates the "registers no shutdown hook" claim in §5 and the matching §11a non-finding. - **A report that cannot be routed to exactly one §13 disposition** — evidence of a model gap; revise the model rather than making an ad-hoc call. --- @@ -332,7 +333,7 @@ The operative test is *content vs. volume*: one well-formed, in-limits request d **Q2 — The CORS defaults. ANSWERED *(maintainer)*.** **Answer:** a compatibility default under §5b. The values arrived with the donated `http-cors` code and existing users depend on them; `cors()` is opt-in, and an application that enables it is expected to configure it, with §10.3 stating what to set. A report that the shipped default should change is `BY-DESIGN: default-configuration` — welcome on the development list, not as a security report. What *is* in scope is the implementation: if `cors()` admits an origin its configuration should have rejected, or emits credentials for one it should not, that is a defect under §5b.4. -**Q3 — File-serving directives.** *Resolved from code — confirm the disposition only.* `safeDirectoryChildPath` contains traversal by two stated measures: a path segment must not be `..` and must not contain `/` or `\\`; and the resolved file's `File.getCanonicalPath` must be prefixed by the base path's. So containment **is** claimed, and a genuine escape from the configured root is `VALID`; passing an unvalidated path in is a §11 misuse. One residual the code comment itself flags: containment rests on `getCanonicalPath`, whose symlink resolution is platform-dependent — *is a symlink out of the served root a `VALID` finding, or an operator responsibility?* *(documented — `FileAndResourceDirectives.scala:229-274`)* +**Q3 — File-serving directives. ANSWERED *(maintainer)*.** *Resolved from code — and, like Q4 and Q5, a correction: the draft asserted containment held, and reviewing the check found a defect in it.* `safeDirectoryChildPath` claims containment by two measures: a path segment must not be `..` and must not contain `/` or `\\`; and the resolved file's `File.getCanonicalPath` must be prefixed by the base path's. The second measure compared the canonical paths as **strings** (`canonicalFinalPath.startsWith(...)`), and a string prefix is not a path prefix: with `/var/www` served, `/var/www-private/secret` satisfies it without being below the served root. The segment filter cannot catch this — the path only leaves the root when `getCanonicalPath` resolves a symbolic link into such a sibling, and no segment looks suspicious. **Answer:** containment is claimed, so the escape is `VALID` under §5b.4 — this is precisely the "walked out of" class that section names — and it is fixed by [#1218](https://github.com/apache/pekko-http/pull/1218), which compares canonical paths element by element. That fix also settles the draft's residual question: a symlink resolving out of the served root is *rejected by the containment check*, whatever its target is named, so a working escape is a `VALID` finding rather than an operator responsibility. The operator keeps the surrounding choices — which root is served, and whether symlinks exist under it. Passing an unvalidated path into `getFromFile` directly remains a §11 misuse. *(documented — `FileAndResourceDirectives.scala:229-274`; fix in [#1218](https://github.com/apache/pekko-http/pull/1218))* **Q4 — `X-Forwarded-For` and client identity. ANSWERED *(maintainer)*.** The disposition is confirmed, but the draft's stated basis for it was wrong and is corrected here. Pekko HTTP **does** parse forwarding headers: `extractClientIP` resolves `X-Forwarded-For` (first address) → `X-Real-Ip` → the `remoteAddress` attribute, in that order (`MiscDirectives.scala:142-145`). What is true is that it never does so *implicitly* — `remote-address-attribute` populates the attribute from the socket peer only, and a route gets header-derived values solely because it called `extractClientIP`. @@ -348,7 +349,7 @@ The operative test is *content vs. volume*: one well-formed, in-limits request d On the second half: **in-process termination is supported**, and `HttpsConnectionContext` is a first-class API. It is not, however, the posture the documentation steers production deployments toward — §4's quoted recommendation to front the service with an enterprise-grade routing solution or load balancer applies to TLS as much as to volume defence (§14 Q1), and in such a deployment termination is commonly the proxy's job. Both are supported; the fronted one is what the docs recommend. -**Q9 — The negative claims in §5. ANSWERED *(maintainer)*.** *Resolved from code, then ruled on.* Scanned the main sources of `http-core`, `http`, `parsing`, `http-caching` and `http-cors`: no `addShutdownHook`, no `ProcessBuilder`/`Runtime.exec`, no `Signal`/`SignalHandler`, no file-writing API, no `System.setProperty`/`Security.*`/`setDefault`, and no bind outside the public `Http().bind*` entry points. All claims hold, and §5 cites the scan rather than asserting them. +**Q9 — The negative claims in §5. ANSWERED *(maintainer)*, with one correction.** *Resolved from code, then ruled on.* Scanned the main sources of `http-core`, `http`, `parsing`, `http-caching` and `http-cors`: no `addShutdownHook`, no `ProcessBuilder`/`Runtime.exec`, no `Signal`/`SignalHandler`, no `System.setProperty`/`Security.*`/`setDefault`, and no bind outside the public `Http().bind*` entry points. **The file-writing claim did not survive review:** the scan missed `FileUploadDirectives.scala:178`, where `fileUploadAll` creates temp files with `Files.createTempFile` + `deleteOnExit` and `storeUploadedFile(s)` streams entity bytes to an application-chosen destination. §5 now states the accurate boundary — writes happen only through upload directives the application installs. The remaining claims hold, and §5 cites the scan rather than asserting them. **Answer:** state the boundary here rather than deferring it. The `ActorSystem`'s shutdown hooks are worth **highlighting** in §5 so an integrator is not surprised by them, but the claim this document makes and stands behind is that **Pekko HTTP registers none of its own**. A report that Pekko HTTP installs a shutdown hook is factually wrong (§11a); one about the hooks `CoordinatedShutdown` or Artery register belongs to `apache/pekko`'s model, and is `OUT-OF-MODEL: unsupported-component` here. @@ -384,7 +385,7 @@ Every other document **links** to `SECURITY.md` for the reporting policy and to | `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 | | `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 a canonical-path prefix | `FileAndResourceDirectives.scala` | §9, §14 Q3 | +| `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 | | `remote-address-attribute = off` | `http-core/reference.conf` | §5, §9, §14 Q4 | | `extractClientIP` reads `X-Forwarded-For`/`X-Real-Ip`; `extractDirectClientIP` reads the attribute alone | `MiscDirectives.scala` | §5, §6, §9, §10.5, §11a, §14 Q4 | | CORS: `*` + credentials echoes the request `Origin` | `http-cors/reference.conf` | §5a, §9, §11a, §14 Q2 | From 818c0228ac99d0a3d1e8c32572c07fd03343f07e Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Mon, 31 Aug 2026 14:36:36 +0100 Subject: [PATCH 8/9] =?UTF-8?q?Update=20the=20=C2=A712=20note=20on=20#1217?= =?UTF-8?q?:=20reworked=20to=20a=20CoordinatedShutdown=20task?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation: §12 flagged #1217's raw JVM shutdown hook as invalidating the §5 "registers no shutdown hook" claim on merge. That PR has since been reworked to register its temp-file cleanup as a CoordinatedShutdown task on the actor system instead, so no §5 claim is affected. Modification: Restate the §12 bullet as a recorded near-miss rather than a pending invalidation. Result: §12 matches the current state of #1217 and the §5/§11a claims stand. Tests: Not run - docs only References: Refs #1217 --- THREAT_MODEL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index 762c9dbed..d867ef77b 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -296,7 +296,7 @@ Pekko HTTP therefore takes the following position *(maintainer)*: - Taking on any authentication, authorization, or rate-limiting responsibility currently disclaimed in §9. - A change to the documented "do not expose directly" posture in `security.md`. - Promotion of a §3 module into the supported surface. -- A change to the §5 process-behaviour claims — for example, [#1217](https://github.com/apache/pekko-http/pull/1217) proposes replacing the per-upload `deleteOnExit` with a single JVM shutdown hook registered by Pekko HTTP itself, which on merge invalidates the "registers no shutdown hook" claim in §5 and the matching §11a non-finding. +- A change to the §5 process-behaviour claims. One near-miss is on record: [#1217](https://github.com/apache/pekko-http/pull/1217) originally replaced the per-upload `deleteOnExit` with a raw JVM shutdown hook registered by Pekko HTTP itself, which would have invalidated the "registers no shutdown hook" claim in §5 and the matching §11a non-finding; it was reworked in review to register the cleanup as a `CoordinatedShutdown` task on the actor system instead, so the claims stand. - **A report that cannot be routed to exactly one §13 disposition** — evidence of a model gap; revise the model rather than making an ad-hoc call. --- From 12c07b5027adfa5eaa4108f9fc13f843322d5c5c Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Mon, 31 Aug 2026 14:54:02 +0100 Subject: [PATCH 9/9] =?UTF-8?q?Fold=20the=20reworked=20#1217=20and=20#1218?= =?UTF-8?q?=20into=20=C2=A75,=20=C2=A79=20and=20Q3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation: Both in-flight PRs the model references changed shape after review. #1218 gained a documented platform caveat - File.getCanonicalPath does not resolve NTFS symbolic links or junctions on Windows, so the link-escape class stays open there - and now rejects path segments that no file-system path may contain instead of erroring. #1217 replaced the per-file deleteOnExit with one temp directory per actor system removed by a CoordinatedShutdown task. Q3's answer claimed symlink escapes are rejected "whatever its target is named", which overclaims on Windows. Modification: Scope the Q3 and §9 symlink-rejection claims to platforms where canonicalization resolves links, record the Windows residual and the toRealPath follow-up, note the invalid-segment hardening, and update the §5 upload-directive note to describe #1217's per-system directory and CoordinatedShutdown cleanup. Result: The model's containment and file-writing claims match what #1217 and #1218 actually implement, on every platform they address. Tests: Not run - docs only References: Refs #1217, Refs #1218 --- THREAT_MODEL.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index d867ef77b..ed4baa46b 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -96,7 +96,7 @@ Negative claims, rarely written down and therefore verified against the source r - **Binds no port until the application calls a `bind*` method.** Binding is reachable only through the public `Http().bind` / `bindAndHandle*` entry points (`Http.scala:179-292`); nothing binds at class or extension initialization. - **Installs no signal handlers and spawns no child processes.** No `sun.misc.Signal`/`SignalHandler`, `ProcessBuilder` or `Runtime.exec` in the main sources. - **Registers no JVM shutdown hook.** There is no `addShutdownHook` anywhere in Pekko HTTP's main sources. Any hook an integrator observes comes from the actor system, not from here — see the caveat below. -- **Touches the file system only through directives the application installs.** The disk-serving directives (`getFromFile`, `getFromDirectory`) read. The upload directives **write**: `storeUploadedFile(s)` streams entity bytes to a destination the application's function chooses, and `fileUploadAll` buffers each part into a `Files.createTempFile` temp file registered with `deleteOnExit` (`FileUploadDirectives.scala:178-179`). Both are reachable only where the application binds them to a route; nothing writes at initialization or outside an installed directive. Beyond `FileUploadDirectives`, the main sources contain no `FileOutputStream`, `Files.write`, `FileWriter` or `createTempFile`. *(The draft claimed Pekko HTTP writes no files at all; the scan missed the upload directives — corrected in review, §14 Q9.)* +- **Touches the file system only through directives the application installs.** The disk-serving directives (`getFromFile`, `getFromDirectory`) read. The upload directives **write**: `storeUploadedFile(s)` streams entity bytes to a destination the application's function chooses, and `fileUploadAll` buffers each part into a `Files.createTempFile` temp file registered with `deleteOnExit` (`FileUploadDirectives.scala:178-179`; in-flight [#1217](https://github.com/apache/pekko-http/pull/1217) moves those temp files into one directory per actor system, removed by a `CoordinatedShutdown` task after the system drains, in place of the per-file `deleteOnExit` registrations). Both are reachable only where the application binds them to a route; nothing writes at initialization or outside an installed directive. Beyond `FileUploadDirectives`, the main sources contain no `FileOutputStream`, `Files.write`, `FileWriter` or `createTempFile`. *(The draft claimed Pekko HTTP writes no files at all; the scan missed the upload directives — corrected in review, §14 Q9.)* - **Does not mutate process-global state at initialization** — no `System.setProperty`, `Security.setProperty`, `Security.addProvider` or `setDefault(...)`. **One inherited caveat, highlighted because it is easy to misattribute.** The claims above cover Pekko HTTP's own modules, not the `ActorSystem` it runs on. `apache/pekko` *does* register JVM shutdown hooks — one in `CoordinatedShutdown`, and a second in Artery when remoting is enabled — so an integrator running a Pekko HTTP service **will** observe shutdown hooks in the process. They arrive with the actor system and are modeled in the companion document. **Pekko HTTP itself adds none**, and that is the claim this section makes: *"a Pekko HTTP process has no shutdown hook"* is false, while *"Pekko HTTP registers no shutdown hook"* is true and is what §14 Q9 settles. *(maintainer — §14 Q9)* @@ -243,7 +243,7 @@ Pekko HTTP therefore takes the following position *(maintainer)*: - **Slow-loris and connection exhaustion** — `idle-timeout` and `max-connections` (P5) bound what one connection holds and how many are accepted, but exhausting those bounds by volume is disclaimed (§14 Q1). A single connection that evades `idle-timeout` while holding resources is the in-scope version. - **Decompression bombs** in request bodies, where the application enables decoding. - **SSRF** via the client API, where the application takes a URL from a request. -- **Path traversal** in file-serving directives is *not* left to the caller — `safeDirectoryChildPath` claims containment, and a genuine escape from the configured root is `VALID` per §5b.4. Review of this model found exactly such a defect: the containment check compared canonical paths as **strings**, so a symbolic link resolving into a sibling directory whose name shares the served root as a string prefix (`/var/www` vs `/var/www-private`) escaped it — fixed by [#1218](https://github.com/apache/pekko-http/pull/1218), which compares path elements (§14 Q3). What remains the caller's is the surrounding choice: which root is served, and whether the tree under it contains symlinks at all. +- **Path traversal** in file-serving directives is *not* left to the caller — `safeDirectoryChildPath` claims containment, and a genuine escape from the configured root is `VALID` per §5b.4. Review of this model found exactly such a defect: the containment check compared canonical paths as **strings**, so a symbolic link resolving into a sibling directory whose name shares the served root as a string prefix (`/var/www` vs `/var/www-private`) escaped it — fixed by [#1218](https://github.com/apache/pekko-http/pull/1218), which compares path elements (§14 Q3). One platform limit is documented at the function: containment rests on `File.getCanonicalPath`, which on Windows does not resolve NTFS symbolic links or junctions, so the link-escape class stays open there. What remains the caller's is the surrounding choice: which root is served, and whether the tree under it contains symlinks at all — a choice that carries the containment on Windows. - **XXE** in XML marshallers — a property of the underlying parser. --- @@ -333,7 +333,7 @@ The operative test is *content vs. volume*: one well-formed, in-limits request d **Q2 — The CORS defaults. ANSWERED *(maintainer)*.** **Answer:** a compatibility default under §5b. The values arrived with the donated `http-cors` code and existing users depend on them; `cors()` is opt-in, and an application that enables it is expected to configure it, with §10.3 stating what to set. A report that the shipped default should change is `BY-DESIGN: default-configuration` — welcome on the development list, not as a security report. What *is* in scope is the implementation: if `cors()` admits an origin its configuration should have rejected, or emits credentials for one it should not, that is a defect under §5b.4. -**Q3 — File-serving directives. ANSWERED *(maintainer)*.** *Resolved from code — and, like Q4 and Q5, a correction: the draft asserted containment held, and reviewing the check found a defect in it.* `safeDirectoryChildPath` claims containment by two measures: a path segment must not be `..` and must not contain `/` or `\\`; and the resolved file's `File.getCanonicalPath` must be prefixed by the base path's. The second measure compared the canonical paths as **strings** (`canonicalFinalPath.startsWith(...)`), and a string prefix is not a path prefix: with `/var/www` served, `/var/www-private/secret` satisfies it without being below the served root. The segment filter cannot catch this — the path only leaves the root when `getCanonicalPath` resolves a symbolic link into such a sibling, and no segment looks suspicious. **Answer:** containment is claimed, so the escape is `VALID` under §5b.4 — this is precisely the "walked out of" class that section names — and it is fixed by [#1218](https://github.com/apache/pekko-http/pull/1218), which compares canonical paths element by element. That fix also settles the draft's residual question: a symlink resolving out of the served root is *rejected by the containment check*, whatever its target is named, so a working escape is a `VALID` finding rather than an operator responsibility. The operator keeps the surrounding choices — which root is served, and whether symlinks exist under it. Passing an unvalidated path into `getFromFile` directly remains a §11 misuse. *(documented — `FileAndResourceDirectives.scala:229-274`; fix in [#1218](https://github.com/apache/pekko-http/pull/1218))* +**Q3 — File-serving directives. ANSWERED *(maintainer)*.** *Resolved from code — and, like Q4 and Q5, a correction: the draft asserted containment held, and reviewing the check found a defect in it.* `safeDirectoryChildPath` claims containment by two measures: a path segment must not be `..` and must not contain `/` or `\\`; and the resolved file's `File.getCanonicalPath` must be prefixed by the base path's. The second measure compared the canonical paths as **strings** (`canonicalFinalPath.startsWith(...)`), and a string prefix is not a path prefix: with `/var/www` served, `/var/www-private/secret` satisfies it without being below the served root. The segment filter cannot catch this — the path only leaves the root when `getCanonicalPath` resolves a symbolic link into such a sibling, and no segment looks suspicious. **Answer:** containment is claimed, so the escape is `VALID` under §5b.4 — this is precisely the "walked out of" class that section names — and it is fixed by [#1218](https://github.com/apache/pekko-http/pull/1218), which compares canonical paths element by element. That fix also settles the draft's residual question, with one platform caveat the fix now states in the function's scaladoc. Where `File.getCanonicalPath` resolves links (POSIX), a symlink resolving out of the served root is *rejected by the containment check*, whatever its target is named, so a working escape is a `VALID` finding rather than an operator responsibility. On Windows, `getCanonicalPath` does not resolve NTFS symbolic links or junctions, so a link out of the root is not detected there — a known residual (the principled follow-up is a `Path.toRealPath`-based check), and on that platform the operator's choice to serve a link-free tree is what carries the containment. #1218 also hardened the same function's failure mode: a decoded path segment that no file-system path may contain (a NUL byte anywhere, characters such as `<` on Windows) is rejected with the traversal warning instead of escaping to the exception handler as a server error. The operator keeps the surrounding choices — which root is served, and whether symlinks exist under it. Passing an unvalidated path into `getFromFile` directly remains a §11 misuse. *(documented — `FileAndResourceDirectives.scala:229-274`; fix in [#1218](https://github.com/apache/pekko-http/pull/1218))* **Q4 — `X-Forwarded-For` and client identity. ANSWERED *(maintainer)*.** The disposition is confirmed, but the draft's stated basis for it was wrong and is corrected here. Pekko HTTP **does** parse forwarding headers: `extractClientIP` resolves `X-Forwarded-For` (first address) → `X-Real-Ip` → the `remoteAddress` attribute, in that order (`MiscDirectives.scala:142-145`). What is true is that it never does so *implicitly* — `remote-address-attribute` populates the attribute from the socket peer only, and a route gets header-derived values solely because it called `extractClientIP`.