Security Bulletins

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Apache Struts 7.4.0 (released 2026-10-02) running on Apache Tomcat 11.0.26 and Java 21. The bulletin list of the Struts wiki, every bulletin cited here, the announcement pages announce-2026.html, announce-2025.html, announce-2024.html, announce-2023.html, announce-2018.html and announce-2017.html, the End-of-Life page and the security guide were fetched and read on 2026-10-10 (the bulletin list ends at S2-078, published on 2026-10-05), and every CVE id shown was looked up on the CVE record service behind cve.org and, for the recent ones, on NVD before it was written down. The page is the history of how Struts was attacked: the bulletins are the project’s own advisories, one wiki page per vulnerability with the affected versions, the fixed versions, the reporter, the CVE id, a description, a solution, a statement about backward compatibility and a workaround. Reading them as a series shows that a handful of design decisions (OGNL evaluated on text that came from the request, parameters bound without a filter, prefixes in parameter names that choose an action, a multipart parser that handles untrusted headers, plugins that deserialize request bodies, and resources that grow with the input) produced almost every serious entry, and that each of those decisions has a visible countermeasure in Struts 7.4.0. The running scenario is the Bookshelf: the page asks, for a Bookshelf built on an old Struts release, which bulletins applied, and ends with an audit of the Bookshelf built on 7.4.0 that fails the build on a vulnerable version and probes the classic attack shapes against the running application. The related pages of this section are Security Hardening, Security Headers, Debugging, Dev Mode and Logging, Testing, Deployment and Production and Struts 1 Classic and Migration.

How a Bulletin Reads

The wiki page Security Bulletins is an index; each entry is a child page named S2-nnn (S2 for Struts 2, a number in the order of publication). Every bulletin has the same fields, and knowing them makes the pages quick to read.

Field What it tells you

Summary

A one-line statement of the weakness

Who should read this

Who is concerned (all Struts 2 developers, or only users of a plugin or a feature)

Impact of vulnerability

The class of damage: remote code execution, denial of service, disclosure of data, data integrity, bypass of a configured limit

Maximum security rating

Critical, Important, Moderate or Low (see the scale below)

Recommendation

The version to upgrade to, sometimes with a configuration alternative

Affected Software

The version ranges, including the branches that are end-of-life

Reporter

Who found it

CVE Identifier

The CVE id, empty for some old bulletins and for configuration advice such as S2-043

Problem, Solution, Backward compatibility, Workaround

The mechanism, the fix, the compatibility cost of the fix and a mitigation when an upgrade is not possible

The index page defines the four ratings, and the definition is about exploitability, not about the size of the damage.

Rating Meaning in the Struts scale

Critical

Could be exploited by a remote attacker to execute arbitrary code automatically, by worms or scanners, even when developers followed the security advice

Important

Could compromise data or availability; includes easy remote code execution that exists because the developer treated user input as trusted

Moderate

Significant mitigation exists: an unlikely configuration, a rarely used feature, or an authenticated attacker

Low

Believed extremely hard to exploit, or minimal consequences

Three reading habits follow from the fields. First, read the affected ranges against the exact version in the application, because several bulletins exclude the patched maintenance releases of an older branch (S2-029 and S2-032 list "except 2.3.20.3 and 2.3.24.3"). Second, read the "Non-affected" and "Note" sentences: S2-067 says that applications not using FileUploadInterceptor are safe, S2-075 says that applications using the default action mapper, the restful2 mapper or the REST plugin are not affected, and S2-072 says that releases which do not bundle the JSON plugin are not affected. Third, read the workaround as a statement of what the weakness depends on: S2-068 offers a size-limited temporary folder or disabling upload, which shows that the leak needs the upload feature to be on.

How the CVE Ids on This Page Were Checked

The wiki pages are written by the project, but the project’s own announcement pages carry ids that look unusual for the 2026 entries (for example CVE-2026-73631 or CVE-2026-104711), so the ids were not copied from the announcements. Each id was fetched from the CVE record service (the JSON API behind cve.org) and the state, the publication date and the title were compared with the bulletin; NVD was queried for a sample of the same ids and returned a record for each. The result is that every id in the tables below resolves to a PUBLISHED record whose title matches the bulletin. The records of the 2026 bulletins were published on 2026-08-14 and 2026-08-15 (S2-070 to S2-074) and on 2026-10-05 (S2-075 to S2-078); the record of S2-069, CVE-2025-68493, was published on 2026-01-11. Where a bulletin has no id (S2-001, S2-007, S2-043) the page says so instead of guessing, and where the bulletin says "Related to CVE-2017-7525" (S2-055) the id is cited as the related Jackson record, not as a Struts id.

Seven Families of Weakness

The seventy-eight bulletins are not seventy-eight different ideas. Grouped by mechanism they fall into a small number of families, and the family is what the Bookshelf author should remember, because the next bulletin will belong to one of them.

Family Bulletins (examples) Countermeasure in Struts 7.4.0 and in your code

OGNL evaluated on request data through parameter names

S2-003, S2-005, S2-009, S2-020, S2-021

ParametersInterceptor with excludeParams patterns, the OGNL allowlist and member access restrictions, and since 6.4 the @StrutsParameter annotation that makes every bound property opt-in (see Parameter Injection and @StrutsParameter)

OGNL evaluated on request data through values (conversion errors, tags, localization)

S2-007, S2-029, S2-036, S2-053, S2-059, S2-061, S2-062

%{...} only on trusted expressions; the 7.4.0 probe below shows no second evaluation of a request value in the attributes tried, Struts tags instead of raw EL, struts.ognl.expressionMaxLength

Method and action selection by request syntax

S2-016, S2-017, S2-019, S2-032, S2-033, S2-057

Dynamic Method Invocation off (struts.enable.DynamicMethodInvocation=false), the action: and redirect: prefixes off, strict method invocation, a namespace on every package (see Wildcard Mappings & Method Invocation)

Multipart and upload handling

S2-045, S2-046, S2-060, S2-063 to S2-068

The actionFileUpload interceptor with UploadedFilesAware, limits in struts.multipart.*, uploads stored outside the web application (see File Upload and Download)

Plugins that parse or deserialize bodies

S2-052, S2-054, S2-055, S2-056, S2-070 to S2-072, S2-077

Plugin limits (struts.json.maxLength, struts.rest.content.maxLength), no XStream handler registered by default (see JSON Plugin & AJAX and REST Plugin)

Resources that grow with the input

S2-011, S2-034, S2-041, S2-044, S2-047, S2-050, S2-063, S2-064, S2-065, S2-068, S2-073, S2-074, S2-076

Bounded reads, bounded caches, bounded decimal rendering; a fixed locale where the application is not multilingual (S2-074)

Shared state and operational exposure

S2-043, S2-069, S2-070, S2-071, S2-078

No Config Browser plugin in production, no shared formatter state, a modern XML parser setup (the XWork XXE of S2-069)

Reading Order: What to Do When a New Bulletin Appears

A bulletin is useful only if it is compared with the application. The decision is small enough to draw: find the version, read the ranges and the conditions, and then either upgrade or apply the workaround.

flowchart TD A["New S2 bulletin or announcement"] --> B["Which Struts version does the WAR contain?"] B --> C{"Version inside the Affected Software ranges?"} C -- "no" --> D["Record the check and the date"] C -- "yes" --> E{"Feature or plugin from the Problem section in use?"} E -- "no" --> D E -- "yes" --> F{"Fixed release available on the same branch?"} F -- "yes" --> G["Upgrade the struts2-bom, rebuild, run the tests"] F -- "no, EOL branch" --> H["Plan migration to 6.x or 7.x now"] H --> I["Apply the Workaround section in the meantime"] G --> J["Redeploy and probe the old attack shape"] I --> J J --> D

The two boxes on the right are the reason the End-of-Life section further down matters: on an end-of-life branch the "fixed release" box is always empty.

Timeline of the Significant Bulletins

The tables list the bulletins that shaped the framework, in order of publication, with the rating, the affected and fixed versions as the bulletin states them, the CVE id (a link to the CVE record is in the References) and the lesson. Ratings are the "Maximum security rating" field of the bulletin.

2007 to 2013: parameter names and values become OGNL

Bulletin CVE Rating Affected Fixed Lesson

S2-001

none listed

Critical

WebWork 2.1 with altSyntax, 2.2.0 to 2.2.5, Struts 2.0.0 to 2.0.8

2.0.9 (or XWork 2.0.4)

Remote code execution on a form validation error; the first sign that the expression language must not be fed with request text

S2-003

CVE-2008-6504

Critical

2.0.0 to 2.1.8.1

2.2.1

A parameter name could be an OGNL statement, so ParametersInterceptor became the main attack surface

S2-005

CVE-2010-1870

Critical

2.0.0 to 2.1.8.1

2.2.1 (or a configuration change)

Same affected range as S2-003 and fixed in the same release: the parameter filter had to be fixed again, a sign that filtering names one pattern at a time is fragile

S2-007

none listed

Important

2.0.0 to 2.2.3

2.2.3.1

User input evaluated as an OGNL expression when a conversion error occurred: error handling is also input handling

S2-009

CVE-2011-3923

Critical

2.0.0 to 2.3.1.1

2.3.1.2

Another ParameterInterceptor bypass; the family kept returning until the model changed (excluded names, then allowlist and opt-in binding)

S2-013

CVE-2013-1966

Critical

2.0.0 to 2.3.14.1

2.3.14.2

A flaw in the includeParams attribute of the URL and anchor tags allowed remote command execution

S2-016

CVE-2013-2251

Critical

2.0.0 to 2.3.15

2.3.15.1

The action:, redirect: and redirectAction: prefixes in parameter names ran expressions; a convenience syntax in the request is an attack surface

S2-019

CVE-2013-4316

Important

2.0.0 to 2.3.15.1

2.3.15.2

Dynamic Method Invocation became disabled by default, the first time a default was changed to close a family

S2-020

CVE-2014-0050 and CVE-2014-0094

Important

2.0.0 to 2.3.16.1

2.3.16.2

Upgrade of Commons FileUpload against a denial of service, and class excluded from parameters to stop ClassLoader manipulation

The names of the bulletins S2-002 to S2-004, S2-006, S2-008, S2-010 to S2-012, S2-014, S2-015, S2-017, S2-018 and S2-021 to S2-028 are on the index page; most are cross-site scripting issues, token weaknesses (S2-010, S2-023), denial of service through long parameter names (S2-011) and more bypasses of the excluded parameters (S2-021, S2-022, S2-024). The long list from 2012 to 2014 is the reason the old books, written in that period, teach a security model (blocklists in the interceptor) that the framework replaced.

2016 to 2018: method selection, the REST plugin, uploads, namespaces

Bulletin CVE Rating Affected Fixed Lesson

S2-029

CVE-2016-0785

Important

2.0.0 to 2.3.24.1 (except 2.3.20.3)

2.3.20.3, 2.3.24.3, 2.3.28

The first forced double evaluation: %{...} in a tag attribute that re-evaluated the request value

S2-032

CVE-2016-3081

Important

2.3.20 to 2.3.28 (except 2.3.20.3 and 2.3.24.3)

2.3.20.3, 2.3.24.3, 2.3.28.1; disable DMI

The method: prefix with Dynamic Method Invocation enabled ran code; the advice was to switch DMI off

S2-033

CVE-2016-3087

Important

2.3.20 to 2.3.28 (same exceptions)

same as S2-032

The ! operator of the REST plugin with DMI enabled, the same lesson in the plugin

S2-037

CVE-2016-4438

Important

2.3.20 to 2.3.28.1

2.3.29

Remote code execution through the REST plugin: a plugin inherits the weaknesses of the mapper it replaces

S2-045

CVE-2017-5638

Critical

2.3.5 to 2.3.31 and 2.5 to 2.5.10

2.3.32 or 2.5.10.1

Incorrect exception handling and error-message generation in the Jakarta Multipart parser let a crafted Content-Type header run commands; the CVE record notes exploitation in the wild in March 2017

S2-046

CVE-2017-5638

Critical

2.3.5 to 2.3.31 and 2.5 to 2.5.10

2.3.32 or 2.5.10.1

The same CVE; the record lists Content-Type, Content-Disposition and Content-Length as the vectors, so a fix for one header had to cover its siblings

S2-052

CVE-2017-9805

Critical

2.1.6 to 2.3.33 and 2.5 to 2.5.12

2.3.34 or 2.5.13

The REST plugin XStream handler deserialized XML without type filtering; deserialization of request bodies is code execution unless the types are restricted

S2-053

CVE-2017-12611

Moderate

2.0.0 to 2.3.33 and 2.5 to 2.5.10.1

2.3.34 or 2.5.12

An unintended expression in a FreeMarker tag instead of a string literal; templates are code too

S2-055

related to CVE-2017-7525 (Jackson)

Important

2.5 to 2.5.14

2.5.14.1

A deserialization flaw in the Jackson library; a Struts upgrade was the way to ship the library fix

S2-057

CVE-2018-11776

Critical

2.3 to 2.3.34 and 2.5 to 2.5.16

2.3.35 or 2.5.17

With alwaysSelectFullNamespace true (also set by the Convention plugin), a result or s:url without a namespace under a package with no or a wildcard namespace could run code: set a namespace for every package

S2-057 is the bulletin that shows how configuration and code combine: no line of the application was wrong in isolation. The bulletin’s own advice (and the verified 7.4.0 defaults in this page’s audit) is that every package declares its namespace and that the defaults for method invocation stay off. The 7.x line changed the default of struts.mapper.alwaysSelectFullNamespace to true as part of the 6 to 7 migration, which makes the namespace discipline more important, not less.

2019 to 2022: forced evaluation and the excluded packages

Bulletin CVE Rating Affected Fixed Lesson

S2-059

CVE-2019-0230

Important

2.0.0 to 2.5.20

2.5.22

Forced double evaluation of %{...} or ${...} that references user-modifiable input in tag attributes other than value; the bulletin suggests the proactive OGNL injection protection

S2-060

CVE-2019-0233

Moderate

2.0.0 to 2.5.20

2.5.22 (or add java.io. and java.nio. to struts.excludedPackageNames)

An access permission override during upload caused a denial of service: package exclusion lists are part of the API surface

S2-061

CVE-2020-17530

Important

2.0.0 to 2.5.25

2.5.26

The S2-059 fix was incomplete; forced evaluation of raw input in tag attributes still ran code

S2-062

CVE-2021-31805

Important

2.0.0 to 2.5.29

2.5.30

The S2-061 fix was incomplete again, so some attributes still evaluated twice; the CVE record says the fix for CVE-2020-17530 was incomplete

Four bulletins in four years teach one rule, repeated in the security guide: do not put incoming, untrusted user input into a forced expression evaluation. The Bookshelf probe at the end of this page tests that rule on 7.4.0.

2023 to 2026: uploads, resources and the 7.x line

Bulletin CVE Date Rating Affected Fixed Lesson

S2-063

CVE-2023-34149

2023-06

Important

2.0.0 to 6.1.2

2.5.31 or 6.1.2.1

Memory exhaustion through unchecked list bounds in parameters

S2-064

CVE-2023-34396

2023-06

Important

2.0.0 to 6.1.2

2.5.31 or 6.1.2.1

Memory exhaustion because normal form fields in a multipart request had no limit

S2-065

CVE-2023-41835

2023-12

Moderate

2.5.31 and 6.1.2.1 to 6.3.0

2.5.32, 6.1.2.2 or 6.3.0.1

Uploaded files stayed in the save directory when a field exceeded maxStringLength: a rejected request must clean up

S2-066

CVE-2023-50164

2023-12-07

Critical

2.0.0 to 2.3.37 (EOL), 2.5.0 to 2.5.32, 6.0.0 to 6.3.0

2.5.33 or 6.3.0.2

Upload parameters could be manipulated to enable path traversal and, in some circumstances, upload a malicious file that gives code execution

S2-067

CVE-2024-53677

2024-12-10

Critical

2.0.0 to 2.3.37 (EOL), 2.5.0 to 2.5.33 (EOL), 6.0.0 to 6.3.0.2

6.4.0 and a migration to the Action File Upload interceptor

The same weakness through setters for uploaded files mixed with setters for normal fields; the fix is not backward compatible and the old mechanism stays vulnerable

S2-068

CVE-2025-64775

2025-12-01

Important

2.0.0 to 2.3.37 (EOL), 2.5.0 to 2.5.33 (EOL), 6.0.0 to 6.7.4, 7.0.0 to 7.0.3

6.8.0 or 7.1.1

File leak in multipart processing exhausts the disk; workaround: a size-limited temporary folder on its own volume, or no upload

S2-069

CVE-2025-68493

2026-01-11

Important

2.0.0 to 2.3.37 (EOL), 2.5.0 to 2.5.33 (EOL), 6.0.0 to 6.1.0

6.1.1

An XML external entity weakness in the XWork component: disclosure of data, denial of service and server-side request forgery

The upload bulletins, S2-066 and S2-067, deserve their own paragraph because they changed the framework. S2-066 was fixed by patch releases (2.5.33 and 6.3.0.2), but for S2-067 the bulletin’s only solution is a new mechanism, because it concerns the mixing of setters for uploaded files with setters for normal fields, and it says the change is not backward compatible. The fix was a new mechanism in 6.4 (actionFileUpload, with UploadedFilesAware and UploadedFile) and the removal of the old interceptor in 7.0, so an application that upgrades the version number only finds that its upload code compiles and silently receives nothing. The upload page of this section shows the new mechanism with verified examples.

The 2026 Bulletins, S2-070 to S2-078

Nine bulletins in two batches (14 August 2026 and 5 October 2026), on top of S2-069 in January, make ten for 2026. They are mostly Moderate and mostly about bounds, and the second batch follows by three days the 7.4.0 and 6.12.0 releases of 2 October 2026.

Bulletin CVE Rating Affected Fixed What it is

S2-070

CVE-2026-73631

Moderate

7.2.1 only

7.3.0

Per-request parsing state in the JSON plugin shared across concurrent requests; data of one request observable in another and a configured limit not enforced

S2-071

CVE-2026-73632

Low

7.2.1 only

7.3.0

The same kind of shared state on the serialization side of the JSON plugin (SMD and JSON-RPC support)

S2-072

CVE-2026-73633

Moderate

2.1.8 to 2.3.37, 2.5.0 to 2.5.33 (EOL), 6.0.0 to 6.10.0, 7.0.0 to 7.2.1

7.3.0 or 6.11.0

Unbounded read of a JSON request body

S2-073

CVE-2026-73634

Moderate

6.0.0 to 6.10.0, 7.0.0 to 7.2.1

7.3.0 or 6.11.0

Unbounded read of a Content Security Policy violation report

S2-074

CVE-2026-73635

Moderate

2.0.0 to 2.3.37, 2.5.0 to 2.5.33 (EOL), 6.0.0 to 6.10.0, 7.0.0 to 7.2.1

7.3.0 or 6.11.0

The locale of the request chooses the localized-text caches, so a client that varies it grows the caches until the heap is full; applications with a fixed locale are not affected

S2-075

CVE-2026-104711

Moderate

2.0.0 to 2.3.37 (EOL), 2.5.0 to 2.5.33 (EOL), 6.0.0 to 6.11.0, 7.0.0 to 7.3.0 only when the OGNL allowlist is disabled

7.4.0 or 6.12.0, or migrate off the legacy RESTful mapper

OGNL injection in the legacy RESTful action mapper; not affected: default mapper, restful2, REST plugin; Struts 7 with its default allowlist

S2-076

CVE-2026-104712

Moderate

2.5.14 to 2.5.33, 6.0.0 to 6.11.0, 7.0.0 to 7.3.0

7.4.0 or 6.12.0

A request parameter bound to a BigDecimal and rendered through the tag library can yield a response many orders of magnitude larger than the request

S2-077

CVE-2026-104713

Important

2.1.8 to 2.3.37, 2.5.0 to 2.5.33 (EOL), 6.0.0 to 6.11.0, 7.0.0 to 7.3.0

7.4.0 or 6.12.0

Unbounded read of a REST request body

S2-078

CVE-2026-104714

Moderate

2.0.0 to 2.3.37, 2.5.0 to 2.5.33 (EOL), 6.0.0 to 6.11.0, 7.0.0 to 7.3.0

7.4.0 or 6.12.0

A date or time formatter retained for a localized message is used by concurrent requests without isolation, so one user’s value can appear in another user’s response

Five observations come out of these nine entries.

  • A regression can hide in one release. S2-070 and S2-071 affect Struts 7.2.1 and nothing else; the bulletin’s recommendation is 7.3.0. A release number is therefore part of the audit, not only "7.x".

  • Bounded reads are the new baseline. S2-072, S2-073 and S2-077 are the same defect in three places (the JSON plugin, the CSP report endpoint, the REST plugin). Both plugin pages of this section (JSON Plugin and Ajax and REST Plugin) describe the limit constants (struts.json.maxLength and struts.rest.content.maxLength) and the fixed 7.4.0 behaviour.

  • Input that selects a cache key is an input. S2-074 is the localization version of the same idea: the locale of the request chooses the key of a cache, so an attacker chooses the number of entries. The localization page of this section covers the fixed locale. See Localization and i18n.

  • The allowlist did its job. S2-075 is a remote code execution class bug that the bulletin rates Moderate, because a Struts 7 application with the default OGNL allowlist is not affected. The rating is a statement about the mitigation, and the mitigation is a default that the Bookshelf must not switch off.

  • Type conversion renders too. S2-076 shows that the combination of a numeric conversion and a rendered value can amplify a request; see Type Conversion for the conversion side.

Why Struts 2.5 and Older Must Be Upgraded

The End-of-Life page states what end-of-life means: the team no longer provides security patches, bug fixes or updates for that branch, and the project supports only the user mailing list and the issue tracker for supported versions, so an EOL version receives no support from the project at all.

Branch End-of-life date What the bulletins show

Struts 1.x

5 April 2013

No patches by definition of end-of-life; CVE-2014-0114 (BeanUtils class property, ClassLoader manipulation) names Struts 1.x through 1.3.10 as affected. The Struts 1 page of this section covers reading and migrating that code

Struts 2.3.x

12 September 2019

The last 2.3 release named as a fix in the bulletins read for this page is 2.3.35 (S2-057); later bulletins that list the branch name "2.3.37 (EOL)" as affected offer no 2.3 fix

Struts 2.5.x

30 October 2023

The last fix on the branch is S2-066 in 2.5.33 (December 2023); S2-067 (2024), S2-068 (2025), S2-069 (2026) and all of S2-072 to S2-078 list 2.5.0 through 2.5.33 (EOL) as affected and offer no 2.5 fix

The consequence is arithmetic, and it can be read directly from the pages cited above: an application that stays on 2.5.33 is affected by S2-067, S2-068, S2-069, S2-072, S2-074, S2-076, S2-077 and S2-078 with no patched release to move to, and every future bulletin will be added to that list. The End-of-Life page mentions that third-party vendors sell extended security coverage for EOL versions and says the Apache Software Foundation does not endorse any commercial offer; it is a bridge for a migration that is already planned, not a place to stay. The migration is a package rename (javax to jakarta), the removal of the old upload interceptor and the opt-in parameter annotation, which the Struts 7.4.0 pages of this section describe one by one.

Worked Example: Which Bulletins Hit an Old Bookshelf?

The Bookshelf of this documentation was, in the style of the older books, once a Struts 2.5.30 application with the fileUpload interceptor for covers, the REST plugin for a catalogue endpoint and <s:property value="%{...}"/> in a few pages. Reading the affected ranges of the bulletins above against 2.5.30 gives a short audit.

Bulletin Applies to 2.5.30? Reason from the bulletin text

S2-062

No

Affected 2.0.0 to 2.5.29; 2.5.30 is the fixed release (the audit starts one release earlier, at 2.5.29, for that bulletin)

S2-063, S2-064

Yes

Affected 2.0.0 to 6.1.2; fixed in 2.5.31

S2-065

No

Affected only 2.5.31 and 6.1.2.1 to 6.3.0

S2-066

Yes

Affected 2.5.0 to 2.5.32; fixed in 2.5.33

S2-067

Yes, with no fix on 2.5

Affected 2.5.0 to 2.5.33 (EOL); the fix is 6.4.0 with the new upload mechanism

S2-068, S2-069

Yes, with no fix on 2.5

Both list 2.5.0 through 2.5.33 (EOL)

S2-072, S2-074, S2-077, S2-078

Yes (when the plugin or feature is used), no fix on 2.5

Each lists 2.5.0 through 2.5.33 (EOL)

S2-073

No

Affected range starts at 6.0.0; the bulletin says earlier releases do not contain the feature

The table is derived from the published ranges only; it does not claim that each weakness is exploitable in that application, because most bulletins have conditions (the plugin is in use, the feature is on). It is the starting point of the triage in the diagram above, and the next section turns the same idea into a build step.

Worked Example: A Build That Refuses a Vulnerable Struts

The cheapest control is a Maven Enforcer rule that fails the build when any Struts artifact on the classpath is older than the release that closed the latest bulletins. The Bookshelf pom pins one version with the struts2-bom (so every plugin follows it) and adds the bannedDependencies rule below. The rule uses the version range syntax (,7.4.0), which means "anything below 7.4.0".

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-enforcer-plugin</artifactId>
    <version>3.5.0</version>
    <executions>
        <execution>
            <id>no-vulnerable-struts</id>
            <phase>validate</phase>
            <goals><goal>enforce</goal></goals>
            <configuration>
                <rules>
                    <bannedDependencies>
                        <excludes>
                            <exclude>org.apache.struts:*:(,7.4.0)</exclude>
                        </excludes>
                        <message>Struts below 7.4.0 has published security bulletins (S2-075 to S2-078); upgrade the struts2-bom.</message>
                    </bannedDependencies>
                </rules>
            </configuration>
        </execution>
    </executions>
</plugin>

With the property struts.version set to 7.3.0 (the Bookshelf pom reads the version of the BOM from it), the build stops in the validate phase, before any code is compiled, and the output names the artifact and the rule.

$ mvn -B -q validate -Dstruts.version=7.3.0
[ERROR] Rule 0: org.apache.maven.enforcer.rules.dependency.BannedDependencies failed with message:
[ERROR] Struts below 7.4.0 has published security bulletins (S2-075 to S2-078); upgrade the struts2-bom.
[ERROR] org.example:bookshelf-struts:war:1.0.0-SNAPSHOT
[ERROR]    org.apache.struts:struts2-core:jar:7.3.0 <--- banned via the exclude/include list

With the default 7.4.0 the same command prints Rule 0: ...BannedDependencies passed and BUILD SUCCESS; both runs were made on 2026-10-10 with Maven and JDK 21. The rule is a floor, not a scanner: it knows nothing about bulletins that are published after the date it was written, so the floor has to be raised by a person when a new bulletin names a higher fixed version. A dependency checker in the continuous integration pipeline covers the transitive libraries that the bulletins do not name (the Jackson case of S2-055 is the example).

Worked Example: Probing the Classic Attack Shapes on 7.4.0

The second half of the audit asks whether the shapes of the historical attacks still do anything on 7.4.0. The Bookshelf action books has one parameter, filter, declared with @StrutsParameter, and the JSP forced.jsp echoes it in four ways. All probes use harmless arithmetic (7*7 would print 49 if it were evaluated) against the scratch application.

public class BookAction extends ActionSupport {
    private String filter;

    public String getFilter() { return filter; }

    @StrutsParameter
    public void setFilter(String filter) { this.filter = filter; }

    public List<String> getTitles() { return List.of("Dune", "Neuromancer"); }
}
<%@ taglib prefix="s" uri="/struts-tags" %>
<p id="a">[<s:property value="filter"/>]</p>
<p id="b">[<s:property value="%{filter}"/>]</p>
<s:textfield name="q" id="%{filter}" value="x"/>

The probes and what 7.4.0 answered (HTTP status in parentheses; session ids that appear in rewritten links were removed from the output):

Probe (the shape of the bulletin) Request Result on 7.4.0

Expression in a parameter value (S2-007, S2-029 family)

GET /books?filter=%{7*7}

The page prints Filter: %{7*7} literally (200); the value is data

Forced evaluation in the attribute of a tag (S2-029, S2-059, S2-061, S2-062)

GET /forced?filter=%{7*7} with id="%{filter}" on s:textfield

The generated input has id="%{7*7}"; s:property value="%{filter}" prints %{7*7}; no 49 anywhere (200). There is no second evaluation of the value

ClassLoader manipulation through a parameter name (S2-020, S2-021, CVE-2014-0114 shape)

GET /books?class.classLoader.URLs[0]=x&filter=a

The request is answered normally (200), filter is bound (Filter: a), the class property is not

OGNL in the parameter name (S2-003, S2-005, S2-009 shape)

GET /books?(#a=1)(b)=1

Answered normally (200); nothing is evaluated

Dynamic Method Invocation, method: prefix (S2-032)

GET /books?method:execute&filter=zz

Answered by the normal execute (200, Filter: zz); the prefix is ignored

Dynamic Method Invocation with ! (S2-033 shape)

GET /books!execute

404; DMI is disabled by default

redirect: prefix (S2-016, S2-017)

GET /books?redirect:http://example.org/

No redirect (no Location header); the prefix is ignored

Content-Type header carrying an expression (S2-045 shape)

POST /books with header Content-Type: %{7*7}

200, the response does not contain 49; the application’s page is rendered

Malformed multipart Content-Type (S2-045, S2-046 shape)

POST /books with multipart/form-data; boundary=%{7*7} and a short body

200, the page is rendered with an empty filter; the response does not contain 49

The conclusion is limited on purpose. A clean probe proves that one shape no longer works in one configuration; it does not prove that the application is secure, and the bulletins S2-075 to S2-078 show that the dangerous settings are the ones that were switched off, such as the OGNL allowlist (S2-075 says Struts 7 is affected only when the allowlist is disabled). The hardening page of this section lists the settings that must stay at their defaults.

Following the Announcements

The project publishes security news through four channels, and a team should subscribe to at least the first.

Channel What it carries

announcements@struts.apache.org

The announcements mailing list named on the project’s mailing list page, with the subscribe address announcements-subscribe@struts.apache.org (and an unsubscribe address of the same pattern); the Announcements pages below are the web copy of what it announces

The Announcements pages (announce-2026.html and the earlier years)

One entry per release and per bulletin, with an anchor of the form a20261005-s2075; each entry names the bulletin, the CVE id and the fixed versions

The Security Bulletins index on the wiki

The index of the S2 pages, the authoritative list of bulletins

security@struts.apache.org

The private address for reporting a vulnerability (never for ordinary bugs, which go to the issue tracker); the project asks reporters to read the security guideline first

Two practical rules. A bulletin appears on the wiki and in the announcements together, so a delay of days between an announcement and the Bookshelf’s upgrade is a window in which the bulletin is public and the application is not patched; the reporting page says that the project takes an active stance against security problems and denial of service attacks and that it cannot promise workarounds for generic problems such as a client streaming a lot of data; its stated philosophy is to avoid attacks that make the server consume resources in a non-linear relationship to the size of the input, which is the kind of fix the 2026 bulletins describe. Second, a release announcement without a bulletin can still be a security release: the announcement of 7.4.0 and 6.12.0 came three days before the bulletins S2-075 to S2-078 were published, and an application that waited for the bulletin gained nothing.

Beyond the Struts Bulletins

Struts runs inside a container, and the container publishes its own security pages. The Tomcat CVEs that touch JSP and the default servlet (CVE-2024-50379 and CVE-2024-56337 for uploads treated as JSP on case-insensitive file systems, CVE-2024-52318 for the XSS in generated JSPs from unreleased pooled tags, CVE-2025-24813 for the partial PUT, CVE-2017-12617 for a JSP upload by PUT and CVE-2020-1938 "Ghostcat" for AJP) are described with their fixed versions in Security, which also lists the verified table of the container side. The Struts 1 record CVE-2014-0114 belongs to the Struts 1 page of this section. The Jackson record CVE-2017-7525 is the one S2-055 points at, and the general rule it teaches is that a Struts bulletin may be a library bulletin with a Struts version number on it.

Pitfalls

Pitfall Why it hurts What to do

Reading only the "Fixed" column

Several bulletins list branches as affected that have no fix (EOL), and a patch release may exist for another branch only

Read the Affected Software ranges against the exact version in the WAR, then the Recommendation

Treating the rating as the severity of the damage

The scale is about exploitability: S2-075 is a code execution class bug rated Moderate because the default allowlist blocks it

Read the Problem and the "not affected" notes, and keep the defaults that provide the mitigation

Upgrading to "latest 7.x" without the release number

S2-070 and S2-071 affect 7.2.1 only

Pin the struts2-bom to the fixed version named by the newest bulletin

Trusting an old fix to be complete

S2-005 after S2-003, S2-061 after S2-059 and S2-062 after S2-061 show that the first fix is sometimes incomplete

Follow the chain of "similar to" bulletins and take the last fixed version

Keeping the old upload mechanism after upgrading

S2-067 says that keeping the old mechanism keeps the application vulnerable; in 7.x the interceptor is removed and the old code silently receives nothing

Migrate to actionFileUpload and UploadedFilesAware

Copying a CVE id from an announcement without opening the record

Ids differ from habits (the 2026 ids are long) and transcription errors create wrong claims

Look up each id on the CVE record service or NVD and compare the title with the bulletin

Staying on 2.5.x with a commercial extension and no plan

The extension is a bridge for a migration, and the project does not endorse it

Schedule the 6.x or 7.x migration and treat the extension as temporary

Switching off the OGNL allowlist to make an old expression work

S2-075 depends on the allowlist being disabled for Struts 7

Add the package to struts.allowlist.packageNames instead

Leaving Config Browser or devMode in production

S2-043 (Low) says that using the Config Browser plugin in production is itself the problem; the security guide says to restrict it and to disable devMode

Remove the plugin from the production WAR and keep struts.devMode=false

Never raising the Enforcer floor

A floor written in 2026 knows nothing about the next bulletin

Update the rule whenever a bulletin names a higher fixed version, and keep a dated note

What Changed Recently

  • 2023: S2-063 and S2-064 (June) and S2-065 and S2-066 (December) close denial of service and path traversal in upload handling; Struts 2.5.x reaches end-of-life on 30 October 2023.

  • 2024: S2-067 (December) forces the migration to the Action File Upload mechanism of 6.4.

  • 2025: S2-068 (December) names 6.8.0 and 7.1.1 together as the fixed versions, the pattern of the 6.x and 7.x lines patched in parallel that continues in 2026.

  • 2026: S2-069 (January) is an XXE in the XWork component fixed in 6.1.1; S2-070 to S2-074 (August) fix limits and shared state in the 7.2.1 line and are fixed in 7.3.0 and 6.11.0; S2-075 to S2-078 (October) are fixed in 7.4.0 and 6.12.0, released on 2 October 2026. As of 2026-10-10 no bulletin lists 7.4.0 as an affected version.

Where the Books and Old Tutorials Are Outdated

Struts 2 in Action (2008, Struts 2.0) and the Struts 1 book Struts in Action (2002) predate almost every bulletin on this page, so the model they teach is the one the bulletins replaced. The statements below describe what that model was and what the bulletins and the 7.4.0 defaults say today.

  • Parameters are bound to every setter that the request names. The 2.0 model relies on the interceptor to filter dangerous names; the bulletins S2-003, S2-005, S2-009, S2-020 and S2-021 are that filter being bypassed or extended. Struts 6.4 and 7 made binding opt-in with the @StrutsParameter annotation (see the parameter injection page of this section).

  • Dynamic Method Invocation and the !, method:, action: and redirect: syntax are normal. S2-016, S2-019, S2-032 and S2-033 turned them into attack surface; DMI is off by default (S2-019) and the 7.4.0 probe above shows the prefixes ignored.

  • %{...} can be used freely in any tag attribute. S2-029, S2-059, S2-061 and S2-062 are the history of why forced evaluation of request data is dangerous; the security guide’s rule is the up-to-date version.

  • The fileUpload interceptor with File, xxxContentType and xxxFileName setters. S2-066 and S2-067 ended it; the 7.x equivalent is the actionFileUpload interceptor.

  • Struts 1 is a supported choice. It has been end-of-life since 5 April 2013, and CVE-2014-0114 names Struts 1.x through 1.3.10; the Struts 1 book is for reading legacy code, not for starting projects.

  • The version of the example is the version to use. The Struts 2.0 and 2.1 versions of that era are inside the affected ranges of S2-003 (2.0.0 to 2.1.8.1) and of every later bulletin that begins at 2.0.0.

  • Security is a chapter, not a build step. A build that fails on a vulnerable dependency, like the Enforcer rule above, is a modern habit that the books do not show.

References