| <!-- |
| Licensed to the Apache Software Foundation (ASF) under one or more |
| contributor license agreements. See the NOTICE file distributed with |
| this work for additional information regarding copyright ownership. |
| The ASF licenses this file to You under the Apache License, Version 2.0 |
| (the "License"); you may not use this file except in compliance with |
| the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| --> |
| # Apache Struts — Threat Model (v0 draft) |
| |
| ## §1 Header |
| |
| - **Project:** Apache Struts (`apache/struts`), `main` @ HEAD (2026-06). Scope: the |
| Struts framework in `apache/struts` only (the core MVC framework, its |
| interceptors, tags, and the plugins shipped in this repo). |
| - **Date:** 2026-06-24. **Drafted for PMC review** via the threat-model-producer |
| rubric (Scovetta). This is an unratified proposal, not an ASF Security team or |
| PMC position; authorship and sponsorship are settled only once the PMC adopts it |
| (see Status below and §14). |
| - **Status:** DRAFT — not yet reviewed by the Struts PMC. Built as a strict |
| superset of the existing [`SECURITY.md`](SECURITY.md) and the published |
| [Struts security guidance](https://struts.apache.org/security/); every |
| load-bearing claim is tagged for provenance (see §14 for open questions). |
| - **Last revised:** 2026-08-15 — re-baselined against Security Bulletins |
| **S2-070 … S2-074**, published 2026-08-14. This fired the §12 trigger twice over: |
| those bulletins added default-hardening controls (7.3.0 / 6.11.0, §5a) and named two |
| vulnerability classes the v0 draft did not carry as properties at all (§8.6, §8.7). |
| Bulletin-to-section map in §16. |
| - **Version binding:** versioned with the project; a report against version *N* |
| is triaged against the model as it stood at *N*. The security envelope changed |
| materially at **7.0** (several hardening knobs flipped to secure-by-default) and |
| again at **7.3.0 / 6.11.0** (the first resource-bound controls — §5a), so the version |
| is itself load-bearing. |
| - **Reporting cross-reference:** §8-property violations → report privately per |
| [`SECURITY.md`](SECURITY.md) (`security@struts.apache.org`); §3/§9/§11a findings |
| are closed citing this document and the existing `SECURITY.md` "Before |
| Reporting" checks. |
| - **Provenance legend:** *(documented)* = Struts' own docs/`SECURITY.md`/security |
| site; *(maintainer)* = confirmed by a Struts PMC member through this process; |
| *(inferred)* = reasoned from architecture/docs, not yet PMC-ratified — each has |
| a matching §14 open question. |
| - **Draft confidence:** the bulk is *(documented)* — Struts has an unusually rich |
| published security policy — with a handful of *(inferred)* scoping calls for the |
| PMC to ratify. |
| |
| **What Struts is.** Apache Struts 2 is a **Java MVC web framework** for building |
| server-side web applications. A request flows: servlet filter → action mapping → |
| **interceptor stack** (parameter population, validation, etc.) → **Action** → |
| **result** (typically a JSP/FreeMarker view). Request parameters are bound onto |
| action properties via setters, and view/configuration expressions are evaluated |
| through **OGNL (Object-Graph Navigation Language)** against the **ValueStack**. |
| *(documented — struts.apache.org)* |
| |
| **The framework's own security philosophy (load-bearing).** Struts |
| **"doesn't provide any security mechanism — it is just a pure web framework."** |
| *(documented — [security guidance](https://struts.apache.org/security/))* It is |
| not an authentication, authorization, session-security, or input-sanitisation |
| layer; those are the embedding application's responsibility (§3/§10). What Struts |
| *does* take an active stance on is **not letting its own machinery — chiefly OGNL |
| expression evaluation and request-parameter binding — become an injection vector**. |
| That single sentence shapes the whole model: most "Struts is insecure" reports are |
| either OGNL-injection-class (in model, §8) or application-responsibility (out of |
| model, §3/§11a). |
| |
| Two corrections to that framing, drawn from the project's own published record. |
| First, injection containment is not the only thing the framework owes its own |
| machinery: it also owes **isolation between concurrently-served requests** and |
| **bounds on what a single client can make it allocate or retain** (§8.6, §8.7). |
| Second, OGNL remains the highest-*severity* class but is no longer the high-*volume* |
| one — S2-070 through S2-074 are five consecutive bulletins with no OGNL among them |
| (§16). A triager who expects every real finding to be OGNL-shaped will misroute the |
| ones that are not. |
| |
| ## §2 Scope and intended use |
| |
| Intended deployment: the Struts JARs are a **dependency embedded inside a web |
| application** (a WAR) that the application developer writes, configures, and |
| deploys into a servlet container (Tomcat, Jetty, …) behind the operator's |
| perimeter. Struts is **in-process** with the application; it has no daemon, no |
| listening socket of its own, and no trust boundary against the application code |
| it runs inside. *(documented — it is a framework, not a server.)* |
| |
| **Caller roles.** |
| |
| - **Untrusted HTTP client** — sends requests (parameters, headers, cookies, |
| multipart uploads) to a Struts-backed endpoint. **The primary untrusted boundary.** |
| Struts must treat all request-derived values as hostile. *(documented — the |
| parameter/OGNL hardening exists precisely for this actor.)* |
| - **Application developer** — writes the actions, JSPs, struts.xml/annotations, |
| and chooses the hardening settings (§5a). **Trusted by the framework** — their |
| code and configuration run with the application's privileges. A finding that |
| requires the developer to write unsafe code or disable a default protection is |
| the application's bug, not Struts' (§3). *(documented — the developer-responsibility |
| section of the security guidance.)* |
| - **Operator** — deploys the WAR, sets `devMode` off, restricts dev-only plugins, |
| configures the container and JVM. **Trusted.** *(documented.)* |
| |
| **Component families.** |
| |
| | Family | Entry point | Touches | In model? | |
| | --- | --- | --- | --- | |
| | OGNL evaluation + ValueStack | expression eval for params, tags, results | in-JVM code paths | **In — the central attack surface** *(documented)* | |
| | Parameter binding (`ParametersInterceptor`, `@StrutsParameter`) | request params → action setters | reflection into app objects | **In — primary boundary** *(documented)* | |
| | Interceptor stack (cookie, fileupload, fetch-metadata, COOP/COEP, …) | per-request processing | request data | **In** *(documented)* | |
| | Tag library / JSP & FreeMarker integration | view rendering, expression output | template eval | **In — output-side OGNL/EL** *(documented)* | |
| | File upload (Jakarta multipart) | multipart request parsing | temp files | **In — historical CVE surface** *(documented — S2 bulletins)* | |
| | Request-body readers (JSON body population, CSP violation-report collection) | reads the raw request body | heap, parse state | **In** *(documented — S2-070, S2-072, S2-073)* | |
| | Bundled plugins (REST, JSON, Convention, …) in this repo | extra mappers/result types | request data | **In — same request-trust surface** *(documented — S2-070/071/072 are JSON-plugin bulletins; §14 Q-plugins answered)* | |
| | Config Browser Plugin | exposes internal config | dev-only diagnostic | **In as dev-only** — exposure in prod is operator misconfig (§3/§11a) *(documented)* | |
| | Embedding application's own actions/JSPs/config | the developer's code | as the app | **Out — application responsibility (§3)** *(documented)* | |
| | Examples / showcase / test apps | demo code | n/a | **Out** *(see §3)* | |
| |
| **An optional feature is not a "non-default configuration."** Four of the five most |
| recent bulletins concern surfaces that stay inert until an application turns them on: |
| the JSON plugin's population of actions from a request body, its SMD / JSON-RPC branch |
| (`enableSMD`), and a mapped endpoint collecting CSP violation reports. **None of that |
| is `OUT-OF-MODEL: non-default-config`.** That disposition is for settings the project |
| documents as *unsafe* (`devMode`, a disabled §5a hardening knob); a feature the project |
| documents as *available* is in model at full request-trust level from the moment an |
| application enables it, and the framework owes it the same §8 properties as the default |
| path. Read literally, the v0 draft let a triager close all four — S2-070, S2-071, |
| S2-072, S2-073 — as non-default configuration; this paragraph is what prevents that. |
| *(documented — S2-070/071/072/073; §13, §14 Q-optional.)* |
| |
| ## §3 Out of scope (explicit non-goals) |
| |
| The detailed lists of developer anti-patterns and insecure configurations are |
| maintained in the project's own docs and are **not duplicated here** — this model |
| links to them and assigns each a triage disposition (§13): |
| |
| - **Anything the application developer is responsible for.** Struts provides no |
| security mechanism of its own *(documented)*. The full enumeration — |
| developer-exposed unsafe setters, request parameters used in localization or |
| forced OGNL evaluation, raw `${...}` JSP-EL over untrusted values, direct JSP |
| access, mixing security levels in one namespace — is in the |
| [security guidance](https://struts.apache.org/security/) and |
| [`SECURITY.md`](SECURITY.md). All are `OUT-OF-MODEL: application-responsibility`. |
| - **Findings that only manifest with a documented-insecure / non-default setting** |
| (`devMode=true`, Config Browser Plugin exposed in production, DMI enabled, or a |
| §5a hardening knob turned off) → `OUT-OF-MODEL: non-default-config`. *(documented.)* |
| - **The servlet container, JVM, JDK, and OS**, and the application's own |
| authentication, authorization, session management, CSRF token storage, and |
| transport (TLS). Struts is "a pure web framework," not a security framework. |
| *(documented / inferred — §14 Q-env.)* |
| - **Generic denial of service** — flooding, bandwidth exhaustion, or repeating a URL: |
| cases where the cost to the server stays proportionate and no framework limit is |
| missing or ineffective. The defence is the operator's, ahead of the application |
| (container, reverse proxy, network). *(documented — [`SECURITY.md`](SECURITY.md).)* |
| **This exclusion is narrower than the v0 draft claimed.** That draft read |
| `SECURITY.md`'s "avoid resource consumption non-linear in the size of inputs" as a |
| triage threshold and put everything linear out of model. The project's own bulletins |
| say otherwise: S2-072, S2-073, and S2-074 are all linear and all carry CVEs. The test |
| is **whether a bound exists, not what shape the curve is** (§8.7) — `SECURITY.md` |
| now states this directly. |
| - **Already-disclosed S2-series vulnerabilities** — a duplicate of an existing |
| Security Bulletin/CVE is closed by reference (the |
| [`SECURITY.md` "Before Reporting"](SECURITY.md) checks), not re-triaged. |
| - **Examples, showcase, and test applications** shipped in the repo. *(inferred — §14 Q-scope.)* |
| |
| ## §4 Trust boundaries and data flow |
| |
| ``` |
| Untrusted HTTP request |
| │ params, headers, cookies, multipart |
| ▼ |
| Servlet filter ─► action mapping ─► Interceptor stack ─► Action ─► Result (JSP/FreeMarker) |
| │ │ |
| ParametersInterceptor tag/result OGNL eval |
| binds params to setters against ValueStack |
| │ │ |
| ▼ ▼ |
| OGNL evaluation against the ValueStack ◄── the trust boundary |
| (allowlist / excluded classes+packages / |
| expression length / @StrutsParameter) |
| ``` |
| |
| - **HTTP client → framework** is the one boundary Struts owns. Every request-derived |
| string (parameter *names* as well as *values*, cookie names/values, header values, |
| multipart filenames) is untrusted and may carry an OGNL payload. The framework's |
| job at this boundary is to bind parameters and evaluate expressions **without |
| letting attacker input reach an OGNL evaluation that creates or changes executable |
| code**. *(documented.)* |
| - **Framework → application code** is *not* a trust boundary — Struts runs the |
| developer's actions and templates in-process, fully trusted. *(documented.)* |
| |
| **Reachability precondition (triager's test).** A finding is in-model only if it is |
| reachable by an **untrusted HTTP client against a Struts application that follows the |
| documented secure configuration** (current-version defaults, `devMode` off, dev-only |
| plugins restricted, no developer anti-patterns from §3). A finding that needs |
| `devMode`, a disabled default protection, a developer-introduced unsafe setter, or a |
| documented anti-pattern is `OUT-OF-MODEL`. *(documented/inferred — §14 Q-default.)* |
| |
| ## §5 Assumptions about the environment |
| |
| - A servlet container and a JVM the operator maintains; Struts does not patch or |
| harden them. *(inferred — §14 Q-env.)* |
| - The application is deployed with the **current supported version** (7.x or 6.x per |
| `SECURITY.md`); 2.x is end-of-life and out of support. *(documented — Supported |
| Versions table.)* |
| - The operator runs production with `devMode=false` and dev-only diagnostics (Config |
| Browser Plugin) disabled or access-controlled. *(documented.)* |
| - Struts opens no sockets and makes no outbound connections of its own; any network |
| egress is the application's. *(inferred — §14 Q-egress.)* |
| |
| ## §5a Build-time and configuration variants — **the central knob set** |
| |
| Struts' security envelope is set almost entirely by **runtime configuration**. The |
| **authoritative, current list of every hardening setting (purpose + secure default) |
| lives in the [security guidance](https://struts.apache.org/security/) and is not |
| reproduced here.** Only the triage-load-bearing facts: |
| |
| - The security posture **changed materially at 7.0**, where a cluster of |
| OGNL-injection and parameter-binding defences became **secure-by-default** — |
| notably the OGNL allowlist (`struts.allowlist.enable`), the `@StrutsParameter` |
| annotation requirement (`struts.parameters.requireAnnotations`), excluded |
| classes/packages, the expression-length cap (`struts.ognl.expressionMaxLength`, |
| default 256), and the static-field/proxy/default-package/custom-map disallows. |
| - The envelope moved again at **7.3.0 / 6.11.0**, which added the first *resource-bound* |
| controls — the substance of §8.7, and on by default. The localized-text caches became |
| bounded (`struts.i18n.cacheMaxSize`) and the CSP violation-report read became bounded |
| (`struts.csp.report.maxSize`, default 8192; a configured value outside 1..1048576 is |
| ignored and the default kept). `struts.locale.validateRequestLocale` (default `false`) |
| is the one opt-in of the set: it restricts request-derived locales to the runtime's |
| available-locale set. *(documented — S2-073, S2-074.)* |
| - `struts.devMode` (must be `false` in production) and Dynamic Method Invocation |
| (gated by Strict Method Invocation since 2.5) are the two settings whose *insecure* |
| value most often turns a non-finding into an apparent finding. |
| - The **FetchMetadata / COOP / COEP** interceptors (6.0+) are opt-in cross-origin |
| defences (§8.5). |
| |
| **Insecure-default question (wave 1).** Because the secure posture is the **7.0 |
| default set**, the triage rule needs ratifying: is "a finding that only works with a |
| pre-7.0 default, or with a 7.0 hardening knob turned off" `OUT-OF-MODEL: |
| non-default-config`, with §10 carrying "deploy current version with defaults"? — §14 |
| Q-default. The OGNL **Java Security Manager sandbox** (`-Dognl.security.manager`) is a |
| separate, opt-in defence built on the JDK `SecurityManager`, which has been |
| **deprecated for removal since JDK 17 (JEP 411), disabled by default since JDK 18, |
| and permanently disabled in JDK 24 (JEP 486)** *(documented — JDK release notes)* — |
| so on modern JDKs the model cannot treat it as a relied-upon control (§14 Q-jsm). |
| |
| ## §6 Assumptions about inputs |
| |
| | Surface | Input | Attacker-controllable? | Concern | |
| | --- | --- | --- | --- | |
| | Parameter binding | request parameter **names and values** | **yes** | OGNL injection via crafted names; binding to unsafe setters | |
| | Cookies | cookie names/values (Cookie Interceptor) | **yes** | same OGNL/parameter concerns; checked by accepted/excluded patterns | |
| | Headers | request headers | **yes** | header-driven expression/log paths | |
| | Multipart upload | file content, filename, content-type | **yes** | parser robustness, temp-file handling (S2 history) | |
| | Request body | raw body bytes (JSON body, CSP violation report) | **yes** | must be read under a bound; a limit the framework advertises must bound the read it names (§8.7) | |
| | Expression context | values that reach an OGNL eval (tags, results, forced eval) | **yes if developer feeds untrusted input in** | the core RCE channel | |
| | struts.xml / annotations / action code | framework + app configuration | **no — developer-trusted** | not an attacker surface (§3) | |
| |
| The accepted/excluded pattern checkers (`AcceptedPatternsChecker` / |
| `ExcludedPatternsChecker`, since 2.3.20) validate parameter names/values for the |
| Parameters and Cookie interceptors; a custom override that drops below the framework |
| defaults is a developer error, not a framework flaw. *(documented.)* |
| |
| A request-derived value that becomes a **key in framework-managed state** is an input |
| class in its own right, not merely a value in flight — a request-derived locale keys the |
| framework's localized-text caches, for instance. Such state can be reached through |
| ordinary request handling, not only through an application feature deliberately built on |
| it. When triaging, ask not only "where does this value get evaluated?" but "what does the |
| framework *retain*, keyed on it, and is that bounded?" *(documented — S2-074.)* |
| |
| ## §7 Adversary model |
| |
| - **In scope:** an **untrusted remote HTTP client** with no credentials, able to send |
| arbitrary parameters, headers, cookies, and multipart uploads to any |
| Struts-handled endpoint. Capabilities: craft parameter names/values carrying OGNL, |
| attempt to reach executable-code creation through the ValueStack, pollute |
| parameter binding, exploit a file-upload or multipart parsing bug, or drive framework |
| code into an unbounded allocation or an unbounded retention (§8.7). Goal: **remote |
| code execution via OGNL** (the dominant Struts threat), and secondarily data |
| disclosure — including disclosure *across* concurrent requests — SSRF through |
| framework features, or denial of service. *(documented — the OGNL lineage is the |
| framework's stated central concern.)* |
| - **Two capabilities the v0 draft left implicit.** The client can issue requests |
| **concurrently**, racing any state the framework shares between them (§8.6); and it |
| can **repeat** requests to accumulate framework-side state, where no single request |
| is remarkable and the harm is in the aggregate (§8.7). Single-request reasoning is |
| not sufficient to clear a surface. *(documented — S2-070/071, S2-074.)* |
| - **On-path network attacker** — only where the application/operator has not deployed |
| TLS; transport security is the app's, so this is largely out of model (§3). *(inferred — §14 Q-env.)* |
| - **Out of scope:** the application developer (writes trusted code/config); the |
| operator (deploys, sets devMode/plugins); anyone with container/host/JVM control; |
| and a developer who disables a default protection or follows a documented |
| anti-pattern (§3). *(documented.)* |
| |
| ## §8 Security properties the framework provides |
| |
| *(In the current-version, default-hardening posture; each lists violation symptom + |
| severity. Most are documented controls — the OGNL-injection defences are the core of |
| Struts' security work.)* |
| |
| 1. **OGNL injection containment.** Attacker-supplied request data (parameter names/ |
| values, cookies, headers) must not reach an OGNL evaluation that creates or alters |
| executable code. Enforced in depth by the default controls listed in §5a / the |
| [security guidance](https://struts.apache.org/security/) (allowlist, excluded |
| classes/packages, expression-length cap, static-field/proxy/default-package/ |
| custom-map disallows, excluded node types). *Violation:* a crafted request |
| achieving OGNL-driven code execution (or class-loader/member access beyond the |
| allowlist) on a default-configured current-version app. *Severity:* |
| security-critical (the S2-RCE class). *(documented.)* |
| 2. **Parameter-binding safety (7.0).** Request parameters bind only to setters the |
| developer marked `@StrutsParameter` (to the declared depth); arbitrary deep/nested |
| property traversal is not reachable by default. *Violation:* parameters reaching |
| an unannotated setter, or nesting beyond the declared depth, on a default 7.0 app. |
| *Severity:* critical. *(documented.)* |
| 3. **Method-invocation control.** Dynamic Method Invocation is gated by Strict Method |
| Invocation; a client cannot invoke arbitrary action methods by name when DMI is at |
| its recommended (off/strict) setting. *Violation:* arbitrary method invocation on a |
| default app. *Severity:* high–critical. *(documented.)* |
| 4. **Expression-length and node-type bounds.** OGNL expressions over the configured |
| length (default 256) and forbidden node types are rejected before evaluation. |
| *Violation:* bypass of these bounds. *Severity:* high. *(documented.)* |
| 5. **Cross-origin / fetch-metadata defences (opt-in).** When the FetchMetadata, COOP, |
| and COEP interceptors are enabled, the framework emits/enforces the corresponding |
| `Sec-Fetch-*` and cross-origin isolation behaviour. *Violation:* the interceptor |
| failing to enforce its documented behaviour when enabled. *Severity:* medium–high. |
| *(documented — opt-in since 6.0.)* |
| 6. **Per-request state isolation.** A framework component holding per-request state — |
| parse state, serialization state, buffers — is not shared between requests being |
| served concurrently. *Violation:* data associated with one request becoming |
| observable in another, or a limit that holds for a single request being defeated by |
| racing two. *Severity:* high — it is a disclosure and integrity failure at once, and |
| it can void another §8 control rather than merely leaking. *Note:* this property is |
| violated by an ordinary concurrency bug, with no attacker sophistication required; |
| the same defect harms honest concurrent users. *(documented — S2-070, S2-071.)* |
| 7. **Bounded consumption of request-derived input.** Framework code that reads a request |
| body reads it under a limit, and framework-managed state keyed on request-derived |
| values (caches, maps) is bounded. A limit the framework advertises bounds the |
| operation it appears to govern. *Violation:* a single request making the framework |
| allocate in proportion to its size with no ceiling; unbounded retention accumulated |
| across requests; or a configured limit that does not constrain the read it names. |
| *Severity:* moderate–high (denial of service). *Note:* **linear growth is a |
| violation.** The test is whether a bound exists, not whether the curve bends — see |
| §3, and `SECURITY.md`'s paragraph on the linear cases. *(documented — S2-072, |
| S2-073, S2-074.)* |
| |
| ## §9 Security properties the framework does *not* provide |
| |
| - **No security mechanism in the general sense.** Struts provides no authentication, |
| authorization, session security, CSRF token store, input sanitisation, or output |
| encoding *for the application's own data* — "it is just a pure web framework." |
| *(documented.)* |
| - *False friend:* "Struts has no built-in login/access control" is **by design**, |
| not a vulnerability. |
| - **No protection against developer anti-patterns or non-default config** — unsafe |
| setters, raw `${}` on user input, request params in localization/forced eval, |
| direct JSP access, `devMode` on, disabled hardening (§3/§5a). |
| - **No defence once OGNL evaluation is fed untrusted input by the application |
| itself** (forced expression evaluation on a request value) — that is the developer |
| handing OGNL the attacker's string. *(documented.)* |
| - **No hard anti-DoS guarantee.** The framework bounds what its own code reads and |
| retains (§8.7) and designs against super-linear consumption, but it cannot protect an |
| application from flooding, bandwidth exhaustion, or repeated requests whose cost to |
| the server stays proportionate — that is the operator's to absorb, ahead of the |
| application. The disclaimed property is *availability under generic load*, not |
| *boundedness of framework code*, which §8.7 does provide. *(documented.)* |
| - **The OGNL Java Security Manager sandbox is not a relied-upon control on modern |
| JDKs** (the underlying `SecurityManager` is deprecated for removal since JDK 17 and |
| permanently disabled in JDK 24; see §5a). *(documented.)* |
| - **Auto-generated error pages do not escape action names** (historical S2-006) — the |
| app must define custom error pages; XSS in the default error page is a documented |
| hardening item, not a defended property. *(documented.)* |
| - **Well-known classes (framework):** OGNL/expression injection, multipart/file-upload |
| parsing bugs, and parameter-pollution are the highest-severity recurring classes — but |
| the recent record adds two more that are currently the *more frequent* ones: |
| **unbounded reads and unbounded framework-managed state** (S2-072/073/074) and **state |
| shared across concurrent requests** (S2-070/071). Reflected XSS, CSRF token |
| management, and transport security remain the application's. *(documented — §16.)* |
| |
| ## §10 Downstream (developer + operator) responsibilities |
| |
| The full, authoritative how-to is the [security guidance](https://struts.apache.org/security/) |
| and [`SECURITY.md`](SECURITY.md); in one line: **deploy a current supported version |
| with the default hardening left on, `devMode` off, dev-only plugins restricted, |
| parameter setters annotated, JSPs hidden behind actions, and the application's own |
| authn/authz/CSRF/TLS supplied** (Struts provides none of those). The threat-model |
| value is only that a finding requiring the developer to *violate* one of these is |
| `OUT-OF-MODEL` (§3/§13), not that this list is novel. |
| |
| Two more that the recent bulletins name explicitly, both as workarounds and as standing |
| practice: the **operator** enforces a maximum request-body size ahead of the |
| application, in the reverse proxy or the servlet container, for any endpoint that |
| accepts a body; and the **developer** sets `struts.locale` to a fixed value wherever |
| request-derived locales are not actually needed. Both are defence in depth — they are |
| *not* substitutes for the §8.7 bounds, and a missing framework bound stays a framework |
| finding whether or not the operator happened to configure around it. |
| *(documented — S2-072, S2-073, S2-074.)* |
| |
| ## §11 Known misuse patterns |
| |
| These are the §3 application-responsibility / non-default-config items viewed as |
| "things integrators get wrong" — running `devMode=true` in production or exposing the |
| Config Browser Plugin; disabling a default OGNL/binding protection "to make something |
| work"; exposing unsafe setters to binding; feeding request parameters into forced |
| OGNL evaluation or localization; allowing direct `*.jsp` access or raw `${}` EL on |
| untrusted values; relying on the OGNL Java Security Manager sandbox on modern JDKs. Each |
| is documented in the [security guidance](https://struts.apache.org/security/); the |
| disposition mapping is §11a/§13. |
| |
| ## §11a Known non-findings (recurring false positives) |
| |
| *(Seeded directly from `SECURITY.md` "Before Reporting" — the PMC owns the |
| authoritative list; §14 Q12.)* |
| |
| - **"OGNL/RCE that only works with `devMode=true`."** `OUT-OF-MODEL: non-default-config` |
| — devMode is a development-only setting documented as unsafe for production. |
| - **"An action setter lets me inject a value / reach a dangerous method."** When the |
| setter is developer-exposed without `@StrutsParameter` (7.0), or performs an unsafe |
| side effect, this is `OUT-OF-MODEL: application-responsibility`. In-model only if it |
| bypasses the framework's *default* binding/OGNL protections. |
| - **"Direct JSP access discloses X / executes Y."** App-deployment misconfiguration — |
| JSPs must be hidden behind actions. `OUT-OF-MODEL: application-responsibility`. |
| - **"Raw `${}` EL / forced OGNL eval on my request parameter is exploitable."** The |
| application fed untrusted input to expression evaluation — documented anti-pattern, |
| not a framework flaw. |
| - **"Config Browser Plugin exposes internal configuration."** Dev-only diagnostic; |
| exposing it in production is operator misconfiguration. `OUT-OF-MODEL: non-default-config`. |
| - **"I can enumerate / pass arbitrary parameters."** Parameter binding is the point of |
| the framework; in-model only when it crosses the default annotation/allowlist |
| protections. |
| - **"Generic DoS: I hammered a URL / saturated the pipe."** Not accepted — proportionate |
| cost with no missing framework bound is the operator's to absorb (§3). **But "I sent a |
| large body" does not belong to this bullet by default:** if framework code reads that |
| body into memory with no ceiling, it is a §8.7 violation and `VALID`, as S2-072 and |
| S2-073 both were. Establish that a bound exists before closing on this ground. *(This |
| bullet previously read "I streamed a huge body," which would have closed S2-072.)* |
| - **"A framework cache grew when I sent many distinct values."** In model only where the |
| cache is genuinely *unbounded*, or keyed on an unvalidated and unbounded input set. A |
| bounded cache reaching its ceiling and evicting is working as designed, and a cache |
| keyed on a validated finite set — a locale restricted to the runtime's available |
| locales, for instance — is not unbounded. *(documented — S2-074.)* |
| - **Duplicate of a disclosed S2-series bulletin/CVE** — closed by reference. |
| - **Dependency-tail CVEs** (a transitive jar, e.g. a logging or XML library) from an |
| SCA scan — triage upstream unless Struts' own code reaches the vulnerable path with |
| untrusted input. |
| |
| ## §12 Conditions that would change this model |
| |
| - A change to the default-hardening set (e.g. a new secure-by-default knob, or a |
| default flipped) — re-baseline §5a/§8/§11a. |
| - A new request-facing surface, a new bundled plugin, or a new expression/templating |
| integration with its own trust surface. |
| - A change to how OGNL evaluation, the allowlist, or parameter binding works. |
| - A report that cannot be routed to a §13 disposition → revise §8/§9. |
| - **A published bulletin whose vulnerability class is not already a §8 property** — the |
| clearest signal that the model under-describes what the framework actually guarantees, |
| and the strongest one, because the PMC has already decided the question by issuing the |
| CVE. S2-070 … S2-074 triggered exactly this re-baseline (§16); the check belongs in |
| the release routine, not in the next report's triage. |
| |
| ## §13 Triage dispositions |
| |
| | Disposition | Meaning | Licensed by | |
| | --- | --- | --- | |
| | `VALID` | A §8 property breaks via an untrusted HTTP client on a current-version, default-hardened app. | §8, §6, §7 | |
| | `VALID-HARDENING` | A §11 misuse is too easy, or a default could be tightened. | §11/§5a | |
| | `OUT-OF-MODEL: application-responsibility` | Requires a developer anti-pattern (unsafe setter, raw EL, forced eval, direct JSP) or the app's own authn/authz. | §3/§10 | |
| | `OUT-OF-MODEL: non-default-config` | Only manifests with `devMode`, a dev-only plugin, DMI, or a disabled default protection — i.e. a setting the project documents as *unsafe*. **Not** a feature the project documents as optional and an application has enabled (§2). | §5a, §2 | |
| | `OUT-OF-MODEL: adversary-not-in-scope` | Requires container/host/JVM/developer control. | §7 | |
| | `OUT-OF-MODEL: unsupported-version` | Only affects an end-of-life (2.x) version. | §5 | |
| | `BY-DESIGN: property-disclaimed` | Concerns a property §9 disclaims (no built-in authn/authz/encoding; generic DoS; JSM on JDK21+). | §9 | |
| | `KNOWN-NON-FINDING` | Matches §11a. | §11a | |
| | `DUPLICATE` | Matches a disclosed S2-series bulletin/CVE. | §3 | |
| | `MODEL-GAP` | Unroutable. | triggers §12 | |
| |
| ## §14 Open questions for the maintainers |
| |
| **Answered since v0 — by the project's own published record (S2-070 … S2-074)** |
| |
| These two are carried here rather than deleted: the PMC settled them by issuing CVEs, |
| so what remains is ratifying the *wording*, not the substance. |
| |
| - **Q-dos — answered.** The line is **boundedness, not curve shape**. Framework code |
| that reads or retains request-derived input with no ceiling is in model even where the |
| growth is linear — S2-072, S2-073, and S2-074 all carry CVEs on linear growth — while |
| generic load whose cost stays proportionate is not. Folded into §3, §8.7, §9, and |
| §11a; the matching clarification is now in [`SECURITY.md`](SECURITY.md) so the model |
| is no longer paraphrasing that document into a rule it does not state. |
| - **Q-plugins — answered.** Bundled plugins are in scope at the same request-trust |
| level: S2-070, S2-071, and S2-072 are all JSON-plugin bulletins with CVEs. The |
| follow-on distinction this exposed — optional *feature* versus documented-unsafe |
| *configuration* — is now in §2 and §13, and is put to the PMC as Q-optional below. |
| |
| **Wave 1 — scope, defaults, intended use** |
| |
| - **Q-default.** Confirm the triage baseline is "current supported version (7.x/6.x) |
| with the documented default hardening on, `devMode` off, dev-only plugins |
| restricted" — and that a finding requiring a pre-7.0 default or a disabled hardening |
| knob is `OUT-OF-MODEL: non-default-config`. (§5a/§13.) |
| - **Q-scope.** Confirm the in-scope surface is the framework in `apache/struts` |
| (core + interceptors + tags + bundled plugins), with the embedding application's own |
| actions/JSPs/config, and examples/showcase, out of scope. (§2/§3.) |
| - **Q-philosophy.** Confirm the framing that Struts provides **no security mechanism |
| of its own** beyond OGNL/parameter-binding injection containment — i.e. authn, |
| authz, session security, CSRF token storage, output encoding, and transport are the |
| application's. (§9.) |
| - **Q-env.** Confirm the servlet container, JVM, JDK, and OS are out of scope — Struts |
| does not patch or harden them, and the operator maintains them. (§3/§5.) |
| - **Q-egress.** Confirm Struts opens no sockets and makes no outbound connections of |
| its own, so any network egress (and the SSRF surface it implies) is the |
| application's. (§5/§7.) |
| |
| **Wave 2 — mechanism confirmations** |
| |
| - **Q-ognl.** Confirm the §8.1 list is the authoritative set of default OGNL-injection |
| defences (allowlist, excluded classes/packages/patterns, expression length, |
| static-field/proxy/default-package/custom-map disallows, excluded node types) and |
| that a bypass of any on a default app is `VALID`. (§8.) |
| - **Q-jsm.** Confirm the OGNL Java Security Manager sandbox is **not** a relied-upon |
| control (opt-in, and non-functional on modern JDKs — see §5a), so a report premised |
| on its absence is not a finding. (§5a/§9.) |
| - **Q-concurrency.** Confirm §8.6 as a framework property: per-request state must not be |
| shared between concurrently-served requests, and a violation is `VALID` on its own — |
| including where the only demonstrated consequence is that another framework limit can |
| be raced, with no data shown to cross. (§8.6, from S2-070/071.) |
| - **Q-bounds.** Confirm the §8.7 scope: does it cover *all* framework-managed state |
| keyed on request-derived values, or only the caches and body reads bounded in |
| 7.3.0/6.11.0? A triager needs to know whether an unbounded structure found elsewhere |
| in the framework is `VALID` or `VALID-HARDENING`. (§8.7.) |
| |
| **Wave 3 — surfaces & false-friends** |
| |
| - **Q-optional.** Confirm the §2 rule that a documented-optional feature (JSON body |
| population, `enableSMD`, a mapped CSP-report endpoint, any bundled plugin) is in model |
| once an application enables it, and that `OUT-OF-MODEL: non-default-config` is |
| reserved for settings the project documents as *unsafe*. (§2/§13.) |
| - **Q-plugin-risk.** With Q-plugins answered, the residual: are any bundled plugins |
| (e.g. REST/XML) historically higher-risk enough to warrant their own §8 note? (§2.) |
| - **Q-upload.** Confirm the multipart/file-upload surface (Jakarta) and what the |
| framework guarantees vs. leaves to the container/app. (§2/§6.) |
| - **Q12.** Beyond the `SECURITY.md` "Before Reporting" list already folded into §11a, |
| what do scanners/researchers most often report against Struts that you consider a |
| non-finding? (Feeds §11a.) |
| |
| ## §15 Appendix — existing-policy back-map |
| |
| This `THREAT_MODEL.md` is **additive** — it does not replace |
| [`SECURITY.md`](SECURITY.md) (reporting process, supported versions, "Before |
| Reporting" checks) or the published [security guidance](https://struts.apache.org/security/); |
| both are preserved and remain canonical for the reporting workflow. The discoverability |
| chain is `AGENTS.md` → `SECURITY.md` → this model. Mapping of existing-policy claims to |
| sections: |
| |
| | Existing-policy statement | Threat-model § | |
| | --- | --- | |
| | "Struts doesn't provide any security mechanism — pure web framework" | §1, §9, §13 (`BY-DESIGN`) | |
| | OGNL is the central historical vuln class | §1, §7, §8.1 | |
| | devMode / Config Browser Plugin are dev-only | §3, §5a, §11a | |
| | `@StrutsParameter` / unsafe setters | §6, §8.2, §10, §11a | |
| | Direct JSP access / raw `${}` EL / forced eval / localization | §3, §10, §11a | |
| | Allowlist / excluded classes/packages / expression length (7.0 defaults) | §5a, §8.1 | |
| | DMI / Strict Method Invocation | §5a, §8.3 | |
| | FetchMetadata / COOP / COEP | §5a, §8.5 | |
| | OGNL JSM sandbox (modern-JDK limitation) | §5a, §9 | |
| | Generic DoS not accepted; non-linear-in-input philosophy | §3, §9 | |
| | Unbounded reads/state are in scope even when linear | §3, §8.7, §11a | |
| | Resource bounds (i18n cache size, CSP report size, locale validation) | §5a, §8.7 | |
| | "Before Reporting" duplicate/known-config checks | §3, §11a, §13 (`DUPLICATE`) | |
| | Supported versions (2.x EOL) | §5, §13 (`OUT-OF-MODEL: unsupported-version`) | |
| |
| ## §16 Appendix — recent-bulletin back-map (S2-070 … S2-074) |
| |
| The five bulletins published on **2026-08-14** are the evidence base for the 2026-08-15 |
| revision, and the reason §8 grew two properties. Each is a published |
| [Security Bulletin](https://cwiki.apache.org/confluence/display/WW/Security+Bulletins) |
| on the Struts cwiki, all five published the same day. Ratings are the bulletins' own. |
| |
| | Bulletin | Rating | What it establishes for the model | § | |
| | --- | --- | --- | --- | |
| | **S2-070** (CVE-2026-73631) — shared parsing state, JSON plugin | Moderate | Per-request parse state shared across concurrent requests: cross-request disclosure and integrity loss, **and bypass of a configured limit** — one defect voiding another control | §8.6, §7, §2 | |
| | **S2-071** (CVE-2026-73632) — shared serialization state, JSON plugin | Low | The same on the response side; the `json` *result type* is unaffected because a writer is built per request — the negative control for §8.6 | §8.6, §2 | |
| | **S2-072** (CVE-2026-73633) — unbounded read of a JSON request body | Moderate | Linear DoS with a CVE, and an advertised limit (JSON input length) that did **not** bound the read it appeared to govern | §8.7, §3, §11a | |
| | **S2-073** (CVE-2026-73634) — unbounded read of a CSP violation report | Moderate | Core rather than a plugin; inert until an application maps the endpoint; ordinarily unauthenticated *by design*, since browsers post to it directly | §8.7, §2, §10 | |
| | **S2-074** (CVE-2026-73635) — unbounded localized-text cache growth | Moderate | Affects the **default configuration** rather than an opt-in feature; a request-derived value used as a **cache key**, making unbounded *retention* a class of its own alongside unbounded reads | §8.7, §6, §5a, §11a | |
| |
| Three of the five would have been closed as non-findings by the v0 draft — S2-072 and |
| S2-073 under §11a's "I streamed a huge body," S2-074 under §3's super-linear-only rule — |
| and the remaining two had no §8 property to violate. That is the honest summary of what |
| this revision fixes, and the reason §12 now carries a bulletin-driven re-baseline |
| trigger. |