This is an automated email from the ASF dual-hosted git repository.
lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git
The following commit(s) were added to refs/heads/main by this push:
new 80278c16f docs(security): re-baseline the threat model on
S2-070..S2-074 (#1852)
80278c16f is described below
commit 80278c16f6c193bd4cb7eb7d1501ff69b4b76b9b
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sat Aug 15 08:05:11 2026 +0200
docs(security): re-baseline the threat model on S2-070..S2-074 (#1852)
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 <[email protected]>
---
SECURITY.md | 7 ++
THREAT_MODEL.md | 198 +++++++++++++++++++++++++++++++++++++++++++++++++-------
2 files changed, 182 insertions(+), 23 deletions(-)
diff --git a/SECURITY.md b/SECURITY.md
index 8ba4c4b29..8a89c21b7 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -46,6 +46,13 @@ workarounds to generic problems (such as a client streaming
lots of data to your
the same URL repeatedly). In general, our philosophy is to avoid any attacks
that can cause the server
to consume resources in a non-linear relationship to the size of inputs.
+To be clear about the linear cases: that philosophy describes what we design
against, not a threshold a
+report has to clear before we will accept it. Framework code that reads or
retains request-derived input
+**without any bound** is in scope even where the growth is merely linear, and
we have assigned CVEs for
+exactly that. What stays out of scope is the generic case above — where the
cost to the server is
+proportionate, no framework limit is missing or ineffective, and the defence
belongs to your servlet
+container, reverse proxy, or network.
+
The mailing address is:
[[email protected]](mailto:[email protected])
[General network server security
tips](http://httpd.apache.org/docs/trunk/misc/security_tips.html)
diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md
index 4e3e490f1..c544d7fcb 100644
--- a/THREAT_MODEL.md
+++ b/THREAT_MODEL.md
@@ -29,10 +29,16 @@
superset of the existing [`SECURITY.md`](SECURITY.md) and the published
[Struts security guidance](https://struts.apache.org/security/); every
load-bearing claim is tagged for provenance (see §14 for open questions).
+- **Last revised:** 2026-08-15 — re-baselined against Security Bulletins
+ **S2-070 … S2-074**, published 2026-08-14. This fired the §12 trigger twice
over:
+ those bulletins added default-hardening controls (7.3.0 / 6.11.0, §5a) and
named two
+ vulnerability classes the v0 draft did not carry as properties at all (§8.6,
§8.7).
+ Bulletin-to-section map in §16.
- **Version binding:** versioned with the project; a report against version *N*
is triaged against the model as it stood at *N*. The security envelope
changed
- materially at **7.0** (several hardening knobs flipped to secure-by-default —
- §5a), so the version is itself load-bearing.
+ materially at **7.0** (several hardening knobs flipped to secure-by-default)
and
+ again at **7.3.0 / 6.11.0** (the first resource-bound controls — §5a), so
the version
+ is itself load-bearing.
- **Reporting cross-reference:** §8-property violations → report privately per
[`SECURITY.md`](SECURITY.md) (`[email protected]`); §3/§9/§11a
findings
are closed citing this document and the existing `SECURITY.md` "Before
@@ -64,6 +70,15 @@ That single sentence shapes the whole model: most "Struts is
insecure" reports a
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
@@ -97,11 +112,24 @@ it runs inside. *(documented — it is a framework, not a
server.)*
| 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)* |
+| 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
@@ -122,9 +150,16 @@ links to them and assigns each a triage disposition (§13):
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`](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.)*
+- **Generic denial of service** — flooding, bandwidth exhaustion, or repeating
a URL:
+ cases where the cost to the server stays proportionate and no framework
limit is
+ missing or ineffective. The defence is the operator's, ahead of the
application
+ (container, reverse proxy, network). *(documented —
[`SECURITY.md`](SECURITY.md).)*
+ **This exclusion is narrower than the v0 draft claimed.** That draft read
+ `SECURITY.md`'s "avoid resource consumption non-linear in the size of
inputs" as a
+ triage threshold and put everything linear out of model. The project's own
bulletins
+ say otherwise: S2-072, S2-073, and S2-074 are all linear and all carry CVEs.
The test
+ is **whether a bound exists, not what shape the curve is** (§8.7) —
`SECURITY.md`
+ now states this directly.
- **Already-disclosed S2-series vulnerabilities** — a duplicate of an existing
Security Bulletin/CVE is closed by reference (the
[`SECURITY.md` "Before Reporting"](SECURITY.md) checks), not re-triaged.
@@ -188,6 +223,13 @@ reproduced here.** Only the triage-load-bearing facts:
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.
@@ -212,6 +254,7 @@ so on modern JDKs the model cannot treat it as a
relied-upon control (§14 Q-jsm
| 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) |
@@ -220,17 +263,30 @@ The accepted/excluded pattern checkers
(`AcceptedPatternsChecker` /
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
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
+ 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
@@ -270,6 +326,24 @@ Struts' security work.)*
`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
@@ -285,8 +359,12 @@ Struts' security work.)*
- **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.)*
+- **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.)*
@@ -294,8 +372,11 @@ Struts' security work.)*
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.
+ 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
@@ -307,6 +388,15 @@ authn/authz/CSRF/TLS supplied** (Struts provides none of
those). The threat-mode
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
@@ -339,8 +429,17 @@ authoritative list; §14 Q12.)*
- **"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.
+- **"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
@@ -354,6 +453,11 @@ authoritative list; §14 Q12.)*
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
@@ -362,7 +466,7 @@ authoritative list; §14 Q12.)*
| `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: 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 |
@@ -372,6 +476,22 @@ authoritative list; §14 Q12.)*
## §14 Open questions for the maintainers
+**Answered since v0 — by the project's own published record (S2-070 … S2-074)**
+
+These two are carried here rather than deleted: the PMC settled them by
issuing CVEs,
+so what remains is ratifying the *wording*, not the substance.
+
+- **Q-dos — answered.** The line is **boundedness, not curve shape**.
Framework code
+ that reads or retains request-derived input with no ceiling is in model even
where the
+ growth is linear — S2-072, S2-073, and S2-074 all carry CVEs on linear
growth — while
+ generic load whose cost stays proportionate is not. Folded into §3, §8.7,
§9, and
+ §11a; the matching clarification is now in [`SECURITY.md`](SECURITY.md) so
the model
+ is no longer paraphrasing that document into a rule it does not state.
+- **Q-plugins — answered.** Bundled plugins are in scope at the same
request-trust
+ level: S2-070, S2-071, and S2-072 are all JSON-plugin bulletins with CVEs.
The
+ follow-on distinction this exposed — optional *feature* versus
documented-unsafe
+ *configuration* — is now in §2 and §13, and is put to the PMC as Q-optional
below.
+
**Wave 1 — scope, defaults, intended use**
- **Q-default.** Confirm the triage baseline is "current supported version
(7.x/6.x)
@@ -400,14 +520,23 @@ authoritative list; §14 Q12.)*
- **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.)
+- **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-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-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,
@@ -434,6 +563,29 @@ sections:
| 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 |
+| Generic DoS not accepted; non-linear-in-input philosophy | §3, §9 |
+| Unbounded reads/state are in scope even when linear | §3, §8.7, §11a |
+| Resource bounds (i18n cache size, CSP report size, locale validation) | §5a,
§8.7 |
| "Before Reporting" duplicate/known-config checks | §3, §11a, §13
(`DUPLICATE`) |
| Supported versions (2.x EOL) | §5, §13 (`OUT-OF-MODEL: unsupported-version`)
|
+
+## §16 Appendix — recent-bulletin back-map (S2-070 … S2-074)
+
+The five bulletins published on **2026-08-14** are the evidence base for the
2026-08-15
+revision, and the reason §8 grew two properties. Each is a published
+[Security
Bulletin](https://cwiki.apache.org/confluence/display/WW/Security+Bulletins)
+on the Struts cwiki, all five published the same day. Ratings are the
bulletins' own.
+
+| Bulletin | Rating | What it establishes for the model | § |
+| --- | --- | --- | --- |
+| **S2-070** (CVE-2026-73631) — shared parsing state, JSON plugin | Moderate |
Per-request parse state shared across concurrent requests: cross-request
disclosure and integrity loss, **and bypass of a configured limit** — one
defect voiding another control | §8.6, §7, §2 |
+| **S2-071** (CVE-2026-73632) — shared serialization state, JSON plugin | Low
| The same on the response side; the `json` *result type* is unaffected because
a writer is built per request — the negative control for §8.6 | §8.6, §2 |
+| **S2-072** (CVE-2026-73633) — unbounded read of a JSON request body |
Moderate | Linear DoS with a CVE, and an advertised limit (JSON input length)
that did **not** bound the read it appeared to govern | §8.7, §3, §11a |
+| **S2-073** (CVE-2026-73634) — unbounded read of a CSP violation report |
Moderate | Core rather than a plugin; inert until an application maps the
endpoint; ordinarily unauthenticated *by design*, since browsers post to it
directly | §8.7, §2, §10 |
+| **S2-074** (CVE-2026-73635) — unbounded localized-text cache growth |
Moderate | Affects the **default configuration** rather than an opt-in feature;
a request-derived value used as a **cache key**, making unbounded *retention* a
class of its own alongside unbounded reads | §8.7, §6, §5a, §11a |
+
+Three of the five would have been closed as non-findings by the v0 draft —
S2-072 and
+S2-073 under §11a's "I streamed a huge body," S2-074 under §3's
super-linear-only rule —
+and the remaining two had no §8 property to violate. That is the honest
summary of what
+this revision fixes, and the reason §12 now carries a bulletin-driven
re-baseline
+trigger.