From 40427c356a0f22e86db44eedf4634308b6d0c6a8 Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Fri, 18 Sep 2026 05:55:43 +0200 Subject: [PATCH] docs: state the control test that separates hardening from a vulnerability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SECURITY.md already says an unbounded read is in scope and THREAT_MODEL §8.5 already says a control that fails to enforce its documented behaviour is a violation, but nothing states the rule that connects them: a control the framework claims and does not enforce is a vulnerability; the absence of a control the framework never claimed is hardening. Adding a control therefore moves the line, which is why the same input can be a hardening report before a limit exists and a vulnerability report once the limit can be bypassed. Adds that test to SECURITY.md with the public WW-5618 -> S2-072 sequence as the worked example, a `VALID: control-not-enforced` disposition to §13, and a §11a bullet so a repeat report is re-triaged against any control added since rather than closed as a duplicate. Co-Authored-By: Claude Opus 5 (1M context) --- SECURITY.md | 21 +++++++++++++++++++++ THREAT_MODEL.md | 5 +++++ 2 files changed, 26 insertions(+) diff --git a/SECURITY.md b/SECURITY.md index 8a89c21b70..08d0c53d27 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -53,6 +53,27 @@ exactly that. What stays out of scope is the generic case above — where the co proportionate, no framework limit is missing or ineffective, and the defence belongs to your servlet container, reverse proxy, or network. +### Hardening or vulnerability: the control test + +The question we ask is not "can this input hurt a server" but "does the framework claim a control +here, and does that control do what it says". + +- **No control claimed.** Where the framework has never promised a bound or a check and the defence is + documented as the operator's or the application's (request-body size at the container, authentication, + CSRF tokens), a report that the framework does not supply it is handled as public hardening — a Jira + ticket, no bulletin, no CVE. The reporter is credited on the ticket. +- **Control claimed but not enforced.** Where the framework documents a limit, a filter, or a policy and it + does not hold for the inputs it targets, that is a vulnerability regardless of how the operator has + configured anything else, and it is handled privately per this policy. + +A consequence worth stating: **adding a control moves the line.** The same request that was a hardening +report before a limit existed becomes a vulnerability report if the limit, once added, can be bypassed. +The JSON plugin's input limits were added as hardening in +[WW-5618](https://issues.apache.org/jira/browse/WW-5618) (6.9.0 / 7.2.1); the later finding that the +length limit could be bypassed was CVE-2026-73633 +([S2-072](https://cwiki.apache.org/confluence/display/WW/S2-072)). Those are two findings, not one, and +they were credited separately. + The mailing address is: [security@struts.apache.org](mailto:security@struts.apache.org) [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 c544d7fcbd..ff9562d556 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -440,6 +440,10 @@ authoritative list; §14 Q12.)* 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.)* +- **"This was reported before and closed as hardening."** Not a ground to close. Check + whether a control was added since; if it was and the new report shows it does not + enforce, the report is `VALID: control-not-enforced`, not `DUPLICATE`. *(documented — + WW-5618 → S2-072.)* - **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 @@ -464,6 +468,7 @@ authoritative list; §14 Q12.)* | 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: control-not-enforced` | A documented framework limit, filter, or policy does not hold for the inputs it targets. Independent of default-config arguments — the control's own documentation is the claim being broken. | §8 (esp. §8.4, §8.5, §8.7) | | `VALID-HARDENING` | A §11 misuse is too easy, or a default could be tightened. | §11/§5a | | `OUT-OF-MODEL: application-responsibility` | Requires a developer anti-pattern (unsafe setter, raw EL, forced eval, direct JSP) or the app's own authn/authz. | §3/§10 | | `OUT-OF-MODEL: non-default-config` | Only manifests with `devMode`, a dev-only plugin, DMI, or a disabled default protection — i.e. a setting the project documents as *unsafe*. **Not** a feature the project documents as optional and an application has enabled (§2). | §5a, §2 |