Skip to content

Latest commit

 

History

History
439 lines (383 loc) · 27.7 KB

File metadata and controls

439 lines (383 loc) · 27.7 KB

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).
  • 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 — §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).

§2 Scope and intended use

Intended deployment: the Struts JARs are a dependency embedded inside a web application (a WAR) that the application developer writes, configures, and deploys into a servlet container (Tomcat, Jetty, …) behind the operator's perimeter. Struts is in-process with the application; it has no daemon, no listening socket of its own, and no trust boundary against the application code it runs inside. (documented — it is a framework, not a server.)

Caller roles.

  • Untrusted HTTP client — sends requests (parameters, headers, cookies, multipart uploads) to a Struts-backed endpoint. The primary untrusted boundary. Struts must treat all request-derived values as hostile. (documented — the parameter/OGNL hardening exists precisely for this actor.)
  • Application developer — writes the actions, JSPs, struts.xml/annotations, and chooses the hardening settings (§5a). Trusted by the framework — their code and configuration run with the application's privileges. A finding that requires the developer to write unsafe code or disable a default protection is the application's bug, not Struts' (§3). (documented — the developer-responsibility section of the security guidance.)
  • Operator — deploys the WAR, sets devMode off, restricts dev-only plugins, configures the container and JVM. Trusted. (documented.)

Component families.

Family Entry point Touches In model?
OGNL evaluation + ValueStack expression eval for params, tags, results in-JVM code paths In — the central attack surface (documented)
Parameter binding (ParametersInterceptor, @StrutsParameter) request params → action setters reflection into app objects In — primary boundary (documented)
Interceptor stack (cookie, fileupload, fetch-metadata, COOP/COEP, …) per-request processing request data In (documented)
Tag library / JSP & FreeMarker integration view rendering, expression output template eval In — output-side OGNL/EL (documented)
File upload (Jakarta multipart) multipart request parsing temp files In — historical CVE surface (documented — S2 bulletins)
Bundled plugins (REST, JSON, Convention, …) in this repo extra mappers/result types request data In — same request-trust surface (inferred — §14 Q-plugins)
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)

§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. Per SECURITY.md, generic flooding or large-body streaming is not accepted; only super-linear amplification inside framework code may be in model (§8 / §14 Q-dos). (documented.)
  • 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.
  • struts.devMode (must be false in production) and Dynamic Method Invocation (gated by Strict Method Invocation since 2.5) are the two settings whose insecure value most often turns a non-finding into an apparent finding.
  • The FetchMetadata / COOP / COEP interceptors (6.0+) are opt-in cross-origin defences (§8.5).

Insecure-default question (wave 1). Because the secure posture is the 7.0 default set, the triage rule needs ratifying: is "a finding that only works with a pre-7.0 default, or with a 7.0 hardening knob turned off" OUT-OF-MODEL: non-default-config, with §10 carrying "deploy current version with defaults"? — §14 Q-default. The OGNL Java Security Manager sandbox (-Dognl.security.manager) is a separate, opt-in defence built on the JDK SecurityManager, which has been deprecated for removal since JDK 17 (JEP 411), disabled by default since JDK 18, and permanently disabled in JDK 24 (JEP 486) (documented — JDK release notes) — so on modern JDKs the model cannot treat it as a relied-upon control (§14 Q-jsm).

§6 Assumptions about inputs

Surface Input Attacker-controllable? Concern
Parameter binding request parameter names and values yes OGNL injection via crafted names; binding to unsafe setters
Cookies cookie names/values (Cookie Interceptor) yes same OGNL/parameter concerns; checked by accepted/excluded patterns
Headers request headers yes header-driven expression/log paths
Multipart upload file content, filename, content-type yes parser robustness, temp-file handling (S2 history)
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.)

§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 trigger a super-linear resource path in framework code. Goal: remote code execution via OGNL (the dominant Struts threat), and secondarily data disclosure, SSRF through framework features, or DoS amplification. (documented — the OGNL lineage is the framework's stated central concern.)
  • 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.)

§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 beyond the "avoid super-linear in input size" philosophy; generic flooding/streaming DoS is the operator's to absorb. (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 framework's recurring risk classes; reflected XSS, CSRF token management, and transport security are the application's.

§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.

§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 streamed a huge body / hammered a URL." Not accepted per SECURITY.md; only super-linear amplification inside framework code is considered.
  • 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.

§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. §5a
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

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-dos. Where is the line between "generic DoS we don't accept" and "super-linear amplification inside framework code we do"? Confirm the §3/§8 wording. (§3.)

Wave 3 — surfaces & false-friends

  • Q-plugins. Which bundled plugins (REST, JSON, Convention, …) are in scope at the same request-trust level, and are any (e.g. REST/XML) historically higher-risk and worth 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 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, §8, §9
"Before Reporting" duplicate/known-config checks §3, §11a, §13 (DUPLICATE)
Supported versions (2.x EOL) §5, §13 (OUT-OF-MODEL: unsupported-version)