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).SECURITY.md and the published Struts security guidance; every load-bearing claim is tagged for provenance (see §14 for open questions).SECURITY.md (security@struts.apache.org); §3/§9/§11a findings are closed citing this document and the existing SECURITY.md “Before Reporting” checks.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.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.
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.
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.)
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):
${...} JSP-EL over untrusted values, direct JSP access, mixing security levels in one namespace — is in the security guidance and SECURITY.md. All are OUT-OF-MODEL: application-responsibility.devMode=true, Config Browser Plugin exposed in production, DMI enabled, or a §5a hardening knob turned off) → OUT-OF-MODEL: non-default-config. (documented.)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.SECURITY.md “Before Reporting” checks), not re-triaged.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)
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.)
SECURITY.md); 2.x is end-of-life and out of support. (documented — Supported Versions table.)devMode=false and dev-only diagnostics (Config Browser Plugin) disabled or access-controlled. (documented.)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:
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.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.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).
| 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.)
(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.)
@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.)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.)SECURITY.md's paragraph on the linear cases. (documented — S2-072, S2-073, S2-074.)${} on user input, request params in localization/forced eval, direct JSP access, devMode on, disabled hardening (§3/§5a).SecurityManager is deprecated for removal since JDK 17 and permanently disabled in JDK 24; see §5a). (documented.)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.)
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.
(Seeded directly from SECURITY.md “Before Reporting” — the PMC owns the authoritative list; §14 Q12.)
devMode=true.” OUT-OF-MODEL: non-default-config — devMode is a development-only setting documented as unsafe for production.@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.OUT-OF-MODEL: application-responsibility.${} 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.OUT-OF-MODEL: non-default-config.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.)| 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 |
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.
SECURITY.md so the model is no longer paraphrasing that document into a rule it does not state.Wave 1 — scope, defaults, intended use
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.)apache/struts (core + interceptors + tags + bundled plugins), with the embedding application's own actions/JSPs/config, and examples/showcase, out of scope. (§2/§3.)Wave 2 — mechanism confirmations
VALID. (§8.)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.)VALID or VALID-HARDENING. (§8.7.)Wave 3 — surfaces & false-friends
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.)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.)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) |
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.