The v0 threat model predates the five bulletins published on 2026-08-14, and read against them it does not describe the framework the PMC has been defending. Three of the five would have been closed as non-findings, and the other two had no security property to violate. The DoS boundary is the substantive error. SECURITY.md says two separate things: a disclaimer about generic attacks against any networked server, and a design philosophy about avoiding resource consumption non-linear in the size of inputs. The model fused them into a triage threshold and put everything linear out of scope - so S2-072 landed squarely on §11a's "I streamed a huge body", and the growth in all three DoS bulletins is linear. All three carry CVEs. Restate the line as boundedness rather than curve shape, in §3/§9/§11a, and add the matching paragraph to SECURITY.md so the model stops paraphrasing that document into a rule it does not state. Add the two properties the bulletins show the framework actually owes: §8.6 per-request state isolation (S2-070/071 - shared parse and serialization state across concurrent requests, which in S2-070 also defeated a configured limit) and §8.7 bounded consumption of request-derived input (S2-072/073/074 - bounded body reads, bounded framework-managed state, and a limit that must bound the read it names). §7 gains the two client capabilities these imply, concurrency and repetition; §6 gains the request body as its own surface and a note that a value used as a cache key is an input class of its own. Separate "optional feature" from "non-default configuration" in §2 and §13. Four of the five bulletins concern surfaces that are off until an application enables them, and as written the model let a triager close every one as OUT-OF-MODEL: non-default-config. That disposition is for settings documented as unsafe, not for features documented as available. Record the 7.3.0/6.11.0 hardening in §5a - struts.i18n.cacheMaxSize, struts.csp.report.maxSize, and the opt-in struts.locale.validateRequestLocale - as a second envelope change after 7.0. Q-dos and Q-plugins move to answered, since the PMC settled both by issuing CVEs; Q-concurrency, Q-bounds and Q-optional take their place. §12 gains the trigger that would have caught this earlier: a published bulletin whose class is not already a §8 property. §16 maps each bulletin to the sections it drives. Everything here is sourced from the published bulletins. The bulletins are deliberately terse about mechanism, and this document holds that line: it describes the classes of defect and what they mean for triage, not where the framework was weak or how to reach it. Still a draft, still unratified - the corrections are drawn from the published bulletins, not from a PMC position on the model itself. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
40 KiB
Apache Struts — Threat Model (v0 draft)
§1 Header
- Project: Apache Struts (
apache/struts),main@ HEAD (2026-06). Scope: the Struts framework inapache/strutsonly (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.mdand the published Struts security guidance; 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@struts.apache.org); §3/§9/§11a findings are closed citing this document and the existingSECURITY.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) 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
devModeoff, 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 andSECURITY.md. All areOUT-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.) This exclusion is narrower than the v0 draft claimed. That draft readSECURITY.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.mdnow 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" 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=falseand 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 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@StrutsParameterannotation 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(defaultfalse) 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 befalsein 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.)
- 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 (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.)
- 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.) - 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.)
- 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.)
- 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.) - 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.)
- 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,devModeon, 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
SecurityManageris 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
and 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; 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 isOUT-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.mdso 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,
devModeoff, dev-only plugins restricted" — and that a finding requiring a pre-7.0 default or a disabled hardening knob isOUT-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
VALIDon 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
VALIDorVALID-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 thatOUT-OF-MODEL: non-default-configis 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 (reporting process, supported versions, "Before
Reporting" checks) or the published security guidance;
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 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.