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 and 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 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.) → Actionresult (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 devMode off, restricts dev-only plugins, configures the container and JVM. Trusted. (documented.)

Component families.

FamilyEntry pointTouchesIn model?
OGNL evaluation + ValueStackexpression eval for params, tags, resultsin-JVM code pathsIn — the central attack surface (documented)
Parameter binding (ParametersInterceptor, @StrutsParameter)request params → action settersreflection into app objectsIn — primary boundary (documented)
Interceptor stack (cookie, fileupload, fetch-metadata, COOP/COEP, …)per-request processingrequest dataIn (documented)
Tag library / JSP & FreeMarker integrationview rendering, expression outputtemplate evalIn — output-side OGNL/EL (documented)
File upload (Jakarta multipart)multipart request parsingtemp filesIn — historical CVE surface (documented — S2 bulletins)
Request-body readers (JSON body population, CSP violation-report collection)reads the raw request bodyheap, parse stateIn (documented — S2-070, S2-072, S2-073)
Bundled plugins (REST, JSON, Convention, …) in this repoextra mappers/result typesrequest dataIn — same request-trust surface (documented — S2-070/071/072 are JSON-plugin bulletins; §14 Q-plugins answered)
Config Browser Pluginexposes internal configdev-only diagnosticIn as dev-only — exposure in prod is operator misconfig (§3/§11a) (documented)
Embedding application's own actions/JSPs/configthe developer's codeas the appOut — application responsibility (§3) (documented)
Examples / showcase / test appsdemo coden/aOut (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 and 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.) 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” 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 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

SurfaceInputAttacker-controllable?Concern
Parameter bindingrequest parameter names and valuesyesOGNL injection via crafted names; binding to unsafe setters
Cookiescookie names/values (Cookie Interceptor)yessame OGNL/parameter concerns; checked by accepted/excluded patterns
Headersrequest headersyesheader-driven expression/log paths
Multipart uploadfile content, filename, content-typeyesparser robustness, temp-file handling (S2 history)
Request bodyraw body bytes (JSON body, CSP violation report)yesmust be read under a bound; a limit the framework advertises must bound the read it names (§8.7)
Expression contextvalues that reach an OGNL eval (tags, results, forced eval)yes if developer feeds untrusted input inthe core RCE channel
struts.xml / annotations / action codeframework + app configurationno — developer-trustednot 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 (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 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 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

DispositionMeaningLicensed by
VALIDA §8 property breaks via an untrusted HTTP client on a current-version, default-hardened app.§8, §6, §7
VALID-HARDENINGA §11 misuse is too easy, or a default could be tightened.§11/§5a
OUT-OF-MODEL: application-responsibilityRequires 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-configOnly 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-scopeRequires container/host/JVM/developer control.§7
OUT-OF-MODEL: unsupported-versionOnly affects an end-of-life (2.x) version.§5
BY-DESIGN: property-disclaimedConcerns a property §9 disclaims (no built-in authn/authz/encoding; generic DoS; JSM on JDK21+).§9
KNOWN-NON-FINDINGMatches §11a.§11a
DUPLICATEMatches a disclosed S2-series bulletin/CVE.§3
MODEL-GAPUnroutable.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 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 (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.mdSECURITY.md → this model. Mapping of existing-policy claims to sections:

Existing-policy statementThreat-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.

BulletinRatingWhat it establishes for the model§
S2-070 (CVE-2026-73631) — shared parsing state, JSON pluginModeratePer-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 pluginLowThe 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 bodyModerateLinear 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 reportModerateCore 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 growthModerateAffects 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.