Wildcard Mappings & Method Invocation
|
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, Jakarta Servlet 6.1 and
Java 21, with the configuration read against the Struts 6.0 DTD (why 6.0).
Everything was verified on 2026-10-08: the facts against the Struts Core Developer Guide, the Getting Started tutorial, the
security guide and the 7.4.0 source, and every example by building it into a Maven WAR with struts2-bom 7.4.0 and
calling it with curl on a scratch Tomcat 11.0.26. The outputs shown are the outputs of those runs. A wildcard mapping lets one
<action> element stand for many URLs, and the placeholder {1} carries the part of the URL that the wildcard matched into the
method name, the class name, the results and the parameters. That power is also the page’s main hazard, because a mapping that
turns request text into a method name decides which Java methods a stranger can call. The running scenario is the Bookshelf
of the previous pages, now with a BookCrudAction that can list, create, edit and delete books, and a maintenance method
called purge that must never be reachable from a URL.
Why Wildcards Exist
A conventional mapping names one URL, one class and one method. The Bookshelf with four operations needs four elements that differ only in a method name. A wildcard mapping states the pattern once.
| Approach | Configuration | Cost |
|---|---|---|
One mapping per URL |
|
Grows with every new method; easy to forget one; each is explicit and safe |
One wildcard mapping |
|
Constant size; every method name that matches the pattern becomes a URL, so the allowed set must be restricted |
One regular-expression mapping |
|
Needs a different pattern matcher for the whole application; clean URLs without an extension |
Wildcard mappings come from Struts 1 and its predecessors (see Struts 1 (Classic) & Migration). The Struts 1 mailreader example that the Core Developer Guide still
quotes maps /edit* to Edit{1}Action. In Struts 1 the same need was met with DispatchAction, an action class whose
request parameter named the method to call, and with LookupDispatchAction, which looked the method up from a button label. Struts
2 offers three replacements, in the order the framework prefers them: the method attribute with a wildcard, strict
method invocation with an allowed list, and, deprecated in practice, dynamic method invocation with the bang
character. See the Struts 1 part of the history page for where those
ideas came from.
| Mechanism | Status | Default and note |
|---|---|---|
Wildcard patterns with |
Current |
Default pattern matcher (the |
Wildcard method selection ( |
Current, preferred |
Subject to strict method invocation |
Strict method invocation ( |
Current, on by default |
The DTD default of |
Regular-expression patterns ( |
Current, opt-in |
One pattern matcher per application |
Named variables in namespaces ( |
Current, opt-in |
Mutually exclusive with the wildcard and regex matchers |
Dynamic method invocation, |
Present, off |
|
The |
Present, off |
|
How a URL Is Matched
The StrutsPrepareAndExecuteFilter of the servlet filter chain hands the
request to the ActionMapper. The default mapper splits the path into a namespace and an action name, removes the extension
(action or none, because struts.action.extension is action,, in 7.4.0, so /crud/createBook and
/crud/createBook.action are the same request) and, only when dynamic method invocation is on, splits the name at a bang. The
configuration then looks the name up inside the namespace. The order of that lookup is the part that people get wrong.
The diagram is the whole rule: a name that has no wildcard is found first and is never compared with the patterns.
The Pattern Tokens
A pattern is an action name that contains a special token. A name without a token is a literal and is matched by plain lookup.
| Token | Matches | Example |
|---|---|---|
|
Zero or more characters except the slash |
|
|
Zero or more characters including the slash |
|
|
The backslash escapes the next character, so |
An action name with an asterisk cannot be requested anyway: |
|
The text matched by the N-th wildcard, N from 1 to 9 |
|
|
The whole matched request path |
|
Two properties of the matching are easy to forget. First, a wildcard is not greedy: it matches up to the first occurrence of the
text that follows it. Second, the loose-match rule: when the pattern ends in a wildcard that follows a literal character, such as
Book_*, the pattern is also tried without those last two characters, so Book matches Book_* with an empty {1}. That
rule exists to support the old *!* form with an optional part, and it is what lets the Bookshelf call Book and Book_create
through one mapping.
The Bookshelf Wildcard Mapping
The scratch application has this action class. execute lists, create, edit and delete are the operations, and purge
is the maintenance method that no URL may reach. Setters that carry @StrutsParameter are the only ones that request
parameters may fill (the page Parameter Injection and @StrutsParameter covers that annotation); the class below has none that matter yet.
package org.example.bookshelf;
import org.apache.struts2.ActionSupport;
public class BookCrudAction extends ActionSupport {
private String message = "unset";
@Override
public String execute() {
if ("unset".equals(message)) {
message = "list";
}
return SUCCESS;
}
public String create() { message = "create"; return SUCCESS; }
public String edit() { message = "edit"; return SUCCESS; }
public String delete() { message = "delete"; return SUCCESS; }
/** Maintenance method that was never meant to be reachable from a URL. */
public String purge() { message = "purge: every book deleted"; return SUCCESS; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
}
The view prints the message field, so the examples below show which method ran. It also prints four fields (type, title, author and id) that are empty in most requests; the outputs on this page keep only the interesting part of the line.
/WEB-INF/views/book.jsp: print the name of the method that ran<%@ page contentType="text/plain; charset=UTF-8" %><%@ taglib prefix="s" uri="/struts-tags" %>method=<s:property value="message"/> type=<s:property value="type"/> title=<s:property value="title"/> author=<s:property value="author"/> id=<s:property value="id"/>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
"https://struts.apache.org/dtds/struts-6.0.dtd">
<struts>
<package name="crud" extends="struts-default" namespace="/crud">
<action name="*Book" class="org.example.bookshelf.BookCrudAction" method="{1}">
<result>/WEB-INF/views/book.jsp</result>
<allowed-methods>create,edit,delete</allowed-methods>
</action>
</package>
</struts>
$ curl -s http://localhost:18080/g8wild/crud/createBook.action
method=create
$ curl -s http://localhost:18080/g8wild/crud/editBook.action
method=edit
$ curl -s http://localhost:18080/g8wild/crud/deleteBook.action
method=delete
$ curl -s http://localhost:18080/g8wild/crud/Book
method=list
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8wild/crud/purgeBook.action
404
The last request shows the point of the allowed list. purgeBook matches the pattern, the placeholder would turn into purge, and
the framework refuses because purge is neither the method attribute of a literal mapping nor on any allowed list. The response
is the ordinary "no action" 404, with the text There is no Action mapped for namespace [/crud] and action name [purgeBook].
The URL /crud/Book has nothing in front of Book, so {1} is empty. An empty method becomes execute, and execute is on
the default allowed list (see Where the Allowed List Comes From), which is why the plain Book URL lists the books.
Where {1} Can Be Used
The substitution is done by ActionConfigMatcher.convert on a copy of the matched configuration. In 7.4.0 it replaces the
placeholders in the following places and in no others.
| Place | Example | Effect |
|---|---|---|
|
|
Chooses the Java method; checked against the allowed methods |
|
|
Chooses the action class; see A Class Chosen by a Wildcard and the OGNL Allowlist for the allowlist consequence |
|
|
Static parameter, set before request parameters |
|
|
Maps an outcome from the matched text |
|
|
Location, namespace, |
|
|
Picks the view |
|
|
Chooses the error result |
{1} in the class, the result location and a static parameter<package name="admin" extends="struts-default" namespace="/admin">
<action name="edit*" class="org.example.bookshelf.Edit{1}Action">
<result>/WEB-INF/views/Edit{1}.jsp</result>
</action>
<action name="book/*" class="org.example.bookshelf.BookCrudAction">
<param name="id">{1}</param>
<param name="message">book/{1}</param>
<result>/WEB-INF/views/book.jsp</result>
</action>
</package>
$ curl -s http://localhost:18080/g8wildal/admin/editAuthor
EditAuthor view: EditAuthorAction ran, id=null
$ curl -s http://localhost:18080/g8wild/admin/book/42
method=book/42 id=42
A result can be a whole redirect target. The mapping below sends /go/toabout to the action about of the /pages namespace.
{1} inside a redirectAction result<package name="go" extends="struts-default" namespace="/go">
<action name="to*">
<result type="redirectAction">
<param name="namespace">/pages</param>
<param name="actionName">{1}</param>
</result>
</action>
</package>
$ curl -s -i http://localhost:18080/g8wild/go/toabout | grep -i "HTTP/\|location"
HTTP/1.1 302
Location: /g8wild/pages/about.action
A mapping with no class at all, the last of these placeholder uses, is the catch-all that serves static-looking pages by name. The
framework fills in ActionSupport as the default class, and the result location is built from the matched name.
/pages/about to /WEB-INF/views/about.jsp<package name="pages" extends="struts-default" namespace="/pages">
<action name="*">
<result>/WEB-INF/views/{1}.jsp</result>
</action>
</package>
$ curl -s http://localhost:18080/g8wild/pages/about
page about rendered
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8wild/pages/secret
404
A single * cannot contain a slash, so /pages/../x cannot be built from the name, and the views stay under WEB-INF. A **
in the same position would remove that protection, so do not use it in a result location. The catch-all belongs at the end of
the configuration of its namespace, as the documentation says, because anything declared after it that it also matches is never
reached (see Which Pattern Wins).
Once a wildcard mapping exists, the framework treats the expanded name as an action of its own. The Core Developer Guide says that such a "virtual" mapping can have its own validations, message resources and type converters, just as a hand-written mapping can.
Which Pattern Wins
An exact name always beats a pattern, and a request that matches an exact name never reaches the wildcard matcher. Among several patterns that match, the order of declaration decides, and here the official documentation and the framework disagree.
| Source | Statement |
|---|---|
Core Developer Guide, Wildcard Mappings |
"If more than one pattern matches the last one wins, so less specific patterns must appear before more specific ones" |
|
"Patterns will be matched in the order they were added. The first match wins, so more specific patterns should be defined before less specific patterns"; the loop stops at the first match |
The scratch run |
The first declared pattern that matches wins, so the guide’s sentence is wrong for 7.4.0 |
Two packages with the same two patterns in opposite orders show it.
/order), and the reverse (namespace /order2)<package name="order" extends="struts-default" namespace="/order">
<action name="*" class="org.example.bookshelf.BookCrudAction">
<param name="message">generic mapping, {0}</param>
<result>/WEB-INF/views/book.jsp</result>
</action>
<action name="book*" class="org.example.bookshelf.BookCrudAction">
<param name="message">book* mapping, {1}</param>
<result>/WEB-INF/views/book.jsp</result>
</action>
<action name="bookshelf" class="org.example.bookshelf.BookCrudAction">
<param name="message">exact mapping</param>
<result>/WEB-INF/views/book.jsp</result>
</action>
</package>
<package name="order2" extends="struts-default" namespace="/order2">
<action name="book*" class="org.example.bookshelf.BookCrudAction">
<param name="message">book* mapping, {1}</param>
<result>/WEB-INF/views/book.jsp</result>
</action>
<action name="*" class="org.example.bookshelf.BookCrudAction">
<param name="message">generic mapping, {0}</param>
<result>/WEB-INF/views/book.jsp</result>
</action>
</package>
$ curl -s http://localhost:18080/g8wild/order/bookcase
method=generic mapping, bookcase
$ curl -s http://localhost:18080/g8wild/order/bookshelf
method=exact mapping
$ curl -s http://localhost:18080/g8wild/order2/bookcase
method=book* mapping, case
$ curl -s http://localhost:18080/g8wild/order2/anything
method=generic mapping, anything
So the rule to apply is the one in the framework’s own javadoc: write the specific pattern first and the catch-all last. Physical order is a property of XML; the
Convention plugin has no such order, because annotated classes are found by scanning. Since Struts 7.3.0 the names of annotated
wildcard actions are sorted most specific first before they are registered (WW-3784), with these keys in order: fewer
** tokens, fewer wildcard tokens (a *, a ** run or a {var} group), more literal characters, then alphabetical order as
a tie-breaker. Struts 7.3.0 applied the ** key third and 7.4.0 moves it to the first place, so a/*/* is registered
before a/** (WW-5743). XML mappings are unaffected and keep their declared order.
Non-Greedy Matching and Several Wildcards
A wildcard stops at the first occurrence of what follows it.
ListAccounts and fails for ListSponsors, and two wildcards in one name<package name="star" extends="struts-default" namespace="/star">
<action name="docs/*" class="org.example.bookshelf.BookCrudAction">
<param name="message">docs/* got {1}</param>
<result>/WEB-INF/views/book.jsp</result>
</action>
<action name="all/**" class="org.example.bookshelf.BookCrudAction">
<param name="message">all/** got {1}</param>
<result>/WEB-INF/views/book.jsp</result>
</action>
<action name="List*s" class="org.example.bookshelf.BookCrudAction">
<param name="message">List*s got {1}</param>
<result>/WEB-INF/views/book.jsp</result>
</action>
<action name="two/*/*" class="org.example.bookshelf.BookCrudAction">
<param name="message">two got {1} and {2}</param>
<result>/WEB-INF/views/book.jsp</result>
</action>
</package>
$ curl -s http://localhost:18080/g8wild/star/docs/readme
method=docs/* got readme
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8wild/star/docs/a/b
404
$ curl -s http://localhost:18080/g8wild/star/all/a/b
method=all/** got a/b
$ curl -s http://localhost:18080/g8wild/star/ListAccounts
method=List*s got Account
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8wild/star/ListSponsors
404
$ curl -s http://localhost:18080/g8wild/star/two/a/b
method=two got a and b
ListSponsors fails for the reason the guide gives: the pattern List*s stops at the first s, so {1} becomes Spon and the
configuration that results is ListSpons, which does not exist. Where a name can legitimately contain the following letter, use
the regular-expression matcher of Regular-Expression Patterns.
Prefix and Postfix Styles
The Core Developer Guide shows two naming styles for wildcard methods: the method as a prefix of the action name (*Book, so
createBook) and the method as a postfix after an underscore (Book_*, so Book_create). The postfix style reads like a URL
with a verb and is the one the loose-match rule was written for.
<package name="postfix" extends="struts-default" namespace="/postfix">
<action name="Book_*" class="org.example.bookshelf.BookCrudAction" method="{1}">
<result>/WEB-INF/views/book.jsp</result>
<allowed-methods>create,edit</allowed-methods>
</action>
</package>
$ curl -s http://localhost:18080/g8wild/postfix/Book_create
method=create
$ curl -s http://localhost:18080/g8wild/postfix/Book
method=list
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8wild/postfix/Book_purge
404
Slashes in Action Names
By default an action name has no slash: /admin/book/42 is read as namespace /admin/book, name 42. The Core Developer Guide’s
section "Parameters after the action name" shows how to keep the rest of the path in the name. Two constants are needed.
book/* and all/** possible<constant name="struts.enable.SlashesInActionNames" value="true"/>
<constant name="struts.mapper.alwaysSelectFullNamespace" value="false"/>
The second constant matters because struts.mapper.alwaysSelectFullNamespace is true in the 7.4.0 default.properties (a change of
the 6 to 7 line). With it set to true the mapper takes the whole path before the last slash as the namespace and never looks
for a shorter namespace that has a longer action name.
g8wild and g8wildns)$ curl -s http://localhost:18080/g8wild/admin/book/42
method=book/42 id=42
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8wildns/admin/book/42
404
$ curl -s http://localhost:18080/g8wildns/admin/book/42 | grep -o "There is no Action[^.]*"
There is no Action mapped for namespace [/admin] and action name [42] associated with context path [/g8wildns]
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8wild/admin/book/42/x
404
The constant struts.enable.SlashesInActionNames changes the whole application, not one package. The Core Developer Guide links
the side effects to WW-1383, and the validation-file naming for names with slashes is covered on the page
Validation. The related constant struts.allowed.action.names holds the regular expression that an action name must match; the
default is [a-zA-Z0-9._!/\-]*, and a name that does not match is answered with the default action.
Strict Method Invocation
Strict method invocation (SMI) is the rule that decides which Java methods a request may run. It replaces the older "strict DMI"
and applies to every way of selecting a method: a method attribute with placeholders, a bang URL and a method: parameter.
The check is made by ActionConfig.isAllowedMethod: a method is allowed when it is the method written in the mapping, when it
equals execute for a mapping without a method, or when it is on the allowed list of the action.
Where the Allowed List Comes From
| Source | Scope | Note |
|---|---|---|
The |
That mapping |
A literal such as |
|
That action |
A comma-separated list; it must come after |
|
Every action of the package |
The |
|
That class |
Convention plugin annotation; takes an array of names |
|
Every action of that Java package |
Same annotation |
|
As the list it appears in |
The text after the prefix is a Java regular expression that must match the whole method name, as in the example of Global Lists, Regular Expressions and Annotations |
|
The package |
Any method whose name matches |
Three states exist. With SMI on and no list for an action, only the global list and the mapping’s own method are allowed. With SMI on and a list, the list is added to those. With SMI off, any method name matching the regular expression is allowed.
Three Packages for the Same Mapping
crud package of The Bookshelf Wildcard Mapping, the same wildcard with no list at all, and with SMI turned off<package name="bare" extends="struts-default" namespace="/bare">
<action name="*Book" class="org.example.bookshelf.BookCrudAction" method="{1}">
<result>/WEB-INF/views/book.jsp</result>
</action>
</package>
<package name="open" extends="struts-default" namespace="/open" strict-method-invocation="false">
<action name="*Book" class="org.example.bookshelf.BookCrudAction" method="{1}">
<result>/WEB-INF/views/book.jsp</result>
</action>
</package>
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8wild/bare/createBook
404
$ curl -s http://localhost:18080/g8wild/bare/deleteBook
method=delete
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8wild/bare/listBook
500
$ curl -s http://localhost:18080/g8wild/open/purgeBook
method=purge: every book deleted
The four results teach different things.
-
bare/createBookis refused: with no list,createis not allowed, because the package declares no<allowed-methods>andcreateis not on the default list. -
bare/deleteBookworks with no entry anywhere, becausedeleteis on the global list thatstruts-defaultdeclares. A method calleddeleteis allowed by default in every package that extendsstruts-default. -
bare/listBookis also allowed by the global list, and then fails with HTTP 500 and the messageorg.example.bookshelf.BookCrudAction.list(), aNoSuchMethodException, because the class has nolistmethod. Being allowed is not the same as existing. -
open/purgeBookcalls the maintenance method. The package turned the protection off, and the mapping{1}became the pattern([A-Za-z0-9_$]*). Do not do this to a package whose action has methods that are not meant for URLs.
What Switching SMI Off Allows
The default regular expression ([A-Za-z0-9_$]*) allows every public method whose name uses those characters, including
getters. A getter returns a String in the Bookshelf (getMessage), and the framework treats the returned String as a result
code. The request below executes getMessage in the unprotected package, and the response is the framework complaining that
unset is not a result.
g8wildsmi)$ curl -s http://localhost:18080/g8wildsmi/open/getMessageBook | grep -o "No result defined[^<]*"
No result defined for action org.example.bookshelf.BookCrudAction and result unset
If a package must run with SMI off, narrow the expression with the constant struts.strictMethodInvocation.methodRegex. With
the value ([a-z]*) the same request is refused, because getMessage has an upper-case letter, while purge and create still pass.
<constant name="struts.strictMethodInvocation.methodRegex" value="([a-z]*)"/>
g8wild that sets the constant (g8wildsmi is the same configuration without it)$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8wild/open/getMessageBook
404
$ curl -s http://localhost:18080/g8wild/open/createBook
method=create
The better position is never to turn SMI off. A restricted wildcard mapping, with the three method names in
<allowed-methods>, can be reasoned about; a package-wide regular expression cannot.
Global Lists, Regular Expressions and Annotations
A package can add to the global list, and a list entry can be a regular expression when it starts with regex:.
<package name="regex" extends="struts-default" namespace="/regex">
<global-allowed-methods>regex:(create|edit)</global-allowed-methods>
<action name="*Book" class="org.example.bookshelf.BookCrudAction" method="{1}">
<result>/WEB-INF/views/book.jsp</result>
</action>
</package>
$ curl -s http://localhost:18080/g8wild/regex2/createBook
method=create
$ curl -s http://localhost:18080/g8wild/regex2/editBook
method=edit
$ curl -s http://localhost:18080/g8wild/regex2/deleteBook
method=delete
$ curl -s http://localhost:18080/g8wild/regex2/Book
method=list
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8wild/regex2/purgeBook
404
The list of the package adds to the default list instead of replacing it: deleteBook and the plain Book (which runs execute)
still work although the package names only the regular expression. The 7.4.0 default list is therefore in force for every package that extends struts-default.
The Convention plugin offers the same control with an annotation, either on the class or in package-info.java. The class below
is in the package org.example.bookshelf.action, which the plugin scans.
archive for dynamic invocationpackage org.example.bookshelf.action;
import org.apache.struts2.convention.annotation.Action;
import org.apache.struts2.convention.annotation.AllowedMethods;
import org.apache.struts2.convention.annotation.Result;
@AllowedMethods({"archive"})
@Action(value = "shelf", results = @Result(name = "success", location = "/WEB-INF/views/book.jsp"))
public class ShelfAction extends org.example.bookshelf.BookCrudAction {
public String archive() {
setMessage("archive");
return SUCCESS;
}
}
The effect shows only when a method can be selected from the URL, so the output is in Dynamic Method Invocation.
Ambiguous Action Methods
The security guide has a section, "Ambiguous Action Methods", about actions with a deep class hierarchy that reuse a method name. Its
example has save() in three classes, one of them overloaded with a Double parameter, and says that OGNL cannot decide which
method to call (WW-4405).
public abstract class AbstractAction extends ActionSupport {
protected void save(Double value) {
// some logic
}
}
public class BaseAction extends AbstractAction {
public String save() throws Exception {
save(Double.MAX_VALUE);
return SUCCESS;
}
}
public class RealAction extends BaseAction {
@Override
public String save() throws Exception {
super.save();
return SUCCESS;
}
}
<package name="amb" extends="struts-default" namespace="/amb">
<action name="save" class="org.example.bookshelf.amb.RealAction">
<result type="httpheader"><param name="status">204</param></result>
</action>
</package>
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8wild/amb/save.action
204
On 7.4.0 the example works. The JIRA issue WW-4405 is closed as fixed in 2.5, so the section of the guide is historical: renaming the method, as the guide advises, is still harmless, but it is no longer needed to make the request work.
The DTD Order of allowed-methods
The DTD declares the content of <action> as ((param|result|interceptor-ref|exception-mapping)*,allowed-methods?). The element
<allowed-methods> must therefore come last. An application whose element is first does not start. Tomcat reports the failure
of the filter, with the cause below, and every request then answers 404.
<allowed-methods> precedes <result> (first scratch build)Caused by: The content of element type "action" must match
"((param|result|interceptor-ref|exception-mapping)*,allowed-methods?)".
Source: Struts Configuration DTD 6.0
Dynamic Method Invocation
Dynamic method invocation (DMI) lets the caller name the method in the URL. The bang form book!create.action runs the book
mapping with the method create. The parameter form sends a request parameter whose name starts with method:, as in
book.action?method:create=x, and the value is ignored. Struts uses the second form for buttons: <s:submit method="create"/>
renders as name="method:create".
The Core Developer Guide explains why the framework does not like it. The code scans the action name for a bang and then
tricks the framework into calling another method while the configuration is that of execute, including its validation. The
framework "believes" it is running execute. The wildcard method feature is implemented differently: the framework behaves as
if the mapping Category!create had been written in the file, so the virtual mapping has its own validations, messages and type
converters. For that reason the guide says wildcard method selection is preferred. A DMI request also reaches a method that no
mapping mentions, and the security guide recommends disabling DMI to avoid XSS in the automatically generated error pages that
contain the action name.
| Switch | Effect |
|---|---|
|
|
|
|
|
|
With DMI Off
exact namespace of the scratch app (DMI off) and what the bang and the method: button do there<package name="exact" extends="struts-default" namespace="/exact">
<action name="book" class="org.example.bookshelf.BookCrudAction">
<result>/WEB-INF/views/book.jsp</result>
<allowed-methods>create</allowed-methods>
</action>
<action name="other" class="org.example.bookshelf.BookCrudAction">
<param name="message">other action ran</param>
<result>/WEB-INF/views/book.jsp</result>
</action>
</package>
g8wild (defaults: DMI off, action prefix off)$ curl -s -o /dev/null -w '%{http_code}\n' 'http://localhost:18080/g8wild/exact/book!create.action'
404
$ curl -s -d 'method:create=Create' http://localhost:18080/g8wild/exact/book.action
method=list
$ curl -s -d 'action:other=Other' http://localhost:18080/g8wild/exact/book.action
method=list
With DMI On, Strict Method Invocation Still Applies
DMI is not removed from Struts 7.4.0, and an application that still relies on it has to turn it on. SMI then decides which
methods the URL may name. The scratch application g8wilddmi sets both switches on and uses these mappings.
<constant name="struts.enable.DynamicMethodInvocation" value="true"/>
<constant name="struts.mapper.action.prefix.enabled" value="true"/>
<package name="dmi" extends="struts-default" namespace="/">
<action name="book" class="org.example.bookshelf.BookCrudAction">
<result>/WEB-INF/views/book.jsp</result>
<allowed-methods>create,edit</allowed-methods>
</action>
<action name="other" class="org.example.bookshelf.BookCrudAction">
<param name="message">other action ran</param>
<result>/WEB-INF/views/book.jsp</result>
</action>
</package>
<package name="dmiopen" extends="struts-default" namespace="/open" strict-method-invocation="false">
<action name="book" class="org.example.bookshelf.BookCrudAction">
<result>/WEB-INF/views/book.jsp</result>
</action>
</package>
g8wilddmi$ curl -s 'http://localhost:18080/g8wilddmi/book!create.action'
method=create
$ curl -s 'http://localhost:18080/g8wilddmi/book!delete.action'
method=delete
$ curl -s 'http://localhost:18080/g8wilddmi/book!purge.action' | grep -o "Method purge[^<]*"
Method purge for action book is not allowed!
$ curl -s 'http://localhost:18080/g8wilddmi/book.action?method:create=x'
method=create
$ curl -s 'http://localhost:18080/g8wilddmi/other!create.action' | grep -o "Method create[^<]*"
Method create for action other is not allowed!
$ curl -s -d 'action:other=Other' http://localhost:18080/g8wilddmi/book.action
method=other action ran
$ curl -s 'http://localhost:18080/g8wilddmi/open/book!purge.action'
method=purge: every book deleted
$ curl -s 'http://localhost:18080/g8wilddmi/shelf!archive.action'
method=archive
$ curl -s -o /dev/null -w '%{http_code}\n' 'http://localhost:18080/g8wilddmi/shelf!purge.action'
404
Read the results in order. create is allowed by the list of book; delete is allowed by the global default list; purge is
refused with the message Method purge for action book is not allowed!, which is a different 404 text from the wildcard case
because the action was found. other has no list, so create is refused. The action: prefix runs the other mapping when the
constant allows it. The package with SMI off lets purge through exactly as the wildcard variant did. The Convention action
accepts archive because of @AllowedMethods, and refuses purge, which the subclass inherits as a public method.
Multi-Button Forms Without DMI
The common reason for DMI was a form with several buttons. A form tag of Struts renders the method and action attributes
of <s:submit> as button names with a prefix, and both prefixes are inert by default, so the buttons stop doing anything.
formaction button<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<s:form action="book" namespace="/exact" theme="simple">
<s:submit value="Create" method="create"/>
<s:submit value="Other" action="other" namespace="/exact"/>
<button type="submit" formaction="<s:url action='book' namespace='/exact'/>">Plain</button>
</s:form>
<form id="book" name="book" action="/g8wild/exact/book.action" method="post">
<input type="submit" value="Create" id="book__create" name="method:create"/>
<input type="submit" value="Other" id="book_other" name="action:other" namespace="/exact"/>
<button type="submit" formaction="/g8wild/exact/book.action">Plain</button>
</form>
The first two buttons send method:create and action:other, which the application above ignores. The replacement without DMI
is one form per operation, or a formaction attribute on an HTML button that points at a wildcard URL such as
createBook.action. Both use ordinary mappings with an allowed list.
Recommendation
Keep struts.enable.DynamicMethodInvocation at false. The security guide’s checklist says so (the page Security Hardening collects it), and the tutorial in the Getting
Started guide gives the same advice. Convert any action!method link or method: button to a wildcard mapping with an allowed
list. State-changing methods such as create, edit and delete belong behind POST and a token (see Double-Submit Tokens & Wait Pages), not behind a link that can
be followed from another site; the examples on this page use GET only because curl makes the outputs short.
Regular-Expression Patterns
The default matcher knows only * and **. When the URL should carry named values, with no file extension and with real
regular expressions, set struts.patternMatcher to regex. The Core Developer Guide’s "Advanced Wildcards" section lists four
constants.
| Constant | Value | Reason |
|---|---|---|
|
|
The patterns contain slashes |
|
|
So that the namespace is the shortest match, |
|
|
Selects |
|
|
"Required from 2.5.25+"; the default in the 7.4.0 |
The official example project wildcard-regex of the struts-examples repository sets struts.patternMatcher to regex and uses the 6.5 DTD. A name can contain {FIELD_NAME}, which matches (.*?) and fills the field of that name, or {FIELD_NAME:REGEX} with a Java
regular expression. The groups can still be read as {1}, {2}, and {0} is the whole input.
<struts>
<constant name="struts.enable.SlashesInActionNames" value="true"/>
<constant name="struts.mapper.alwaysSelectFullNamespace" value="false"/>
<constant name="struts.patternMatcher" value="regex"/>
<constant name="struts.matcher.appendNamedParameters" value="true"/>
<package name="books" extends="struts-default" namespace="/">
<action name="/{type}/content/{title}" class="org.example.bookshelf.BookCrudAction">
<result>/WEB-INF/views/book.jsp</result>
</action>
<action name="/{type}/{author:.+}/list" class="org.example.bookshelf.BookCrudAction">
<result>/WEB-INF/views/book.jsp</result>
</action>
<action name="/isbn/{ISBN}/content" class="org.example.bookshelf.BookCrudAction">
<param name="message">isbn {1}</param>
<result>/WEB-INF/views/book.jsp</result>
</action>
<action name="/year/{year:[0-9]+}" class="org.example.bookshelf.BookCrudAction">
<param name="message">year {1}</param>
<result>/WEB-INF/views/book.jsp</result>
</action>
</package>
</struts>
public String getType() { return type; }
public String getTitle() { return title; }
public String getAuthor() { return author; }
@StrutsParameter public void setType(String type) { this.type = type; }
@StrutsParameter public void setTitle(String title) { this.title = title; }
@StrutsParameter public void setAuthor(String author) { this.author = author; }
g8wildrx (the view prints the method and the three fields)$ curl -s http://localhost:18080/g8wildrx/fiction/content/Frankenstein
method=list type=fiction title=Frankenstein author=
$ curl -s http://localhost:18080/g8wildrx/philosophy/AynRand/list
method=list type=philosophy title= author=AynRand
$ curl -s http://localhost:18080/g8wildrx/isbn/978-0441172719/content
method=isbn 978-0441172719
$ curl -s http://localhost:18080/g8wildrx/year/1965
method=year 1965
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8wildrx/year/abc
404
$ curl -s http://localhost:18080/g8wildrx/fiction/content/a/b
method=list type=fiction title=a/b author=
Four properties of this matcher are not in the guide, and the scratch runs showed them.
-
A
{title}with no regular expression matches any text including slashes, so/fiction/content/a/bgives the titlea/b. Add a regular expression such as{title:[^/]+}where that is wrong. -
The
*and**tokens do not exist in this mode. The matcher treats a name with no{as a literal, so every wildcard mapping of the earlier sections would stop working ifstruts.patternMatcherwere switched on its own. -
A pattern whose regular expression contains braces, such as
{year:[0-9]{4}}, stops the application from starting. The matcher ends the expression at the first closing brace, builds the regular expressionyear/([0-9](.*?)and Tomcat reportsPatternSyntaxException: Unclosed group. Write[0-9]+or\d+, or repeat the class. -
Only one pattern matcher exists per application. A project that needs wildcards in some packages and regular expressions in others needs a custom
PatternMatcher, as the guide says.
Named Variables in Namespaces
The third matcher puts variables in the namespace. The guide’s example is @Namespace("/users/{userID}") on an action class,
which makes /users/10/detail run DetailsAction with the field userID set to 10. The XML form is a package namespace.
<constant name="struts.patternMatcher" value="namedVariable"/>
<package name="shelves" extends="struts-default" namespace="/authors/{id}">
<action name="books" class="org.example.bookshelf.BookCrudAction">
<result>/WEB-INF/views/book.jsp</result>
</action>
</package>
g8wildnv$ curl -s http://localhost:18080/g8wildnv/authors/10/books
method=list id=10
$ curl -s http://localhost:18080/g8wildnv/authors/abc/books
method=list id=abc
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8wildnv/authors/books
404
The guide lists the limits. The named-variable matcher and the wildcard matcher cannot be used together, so a * in an action
name stops working. Some tags write the literal namespace with the braces into the HTML (/{user}/2w) instead of the namespace
of the request when they guess a namespace from the current action: <s:form>, <s:url> and action= attributes. The
workaround is a plain HTML tag with an explicit URL. A custom ActionMapper can do the same job, and the default mapper is
the class that calls the pattern matcher.
Path Values and Parameter Binding
A value that the pattern extracts reaches the action in one of two ways, and the two ways have different security rules.
-
As
<param>elements. A{1}in<param name="id">is a static parameter of the mapping. ThestaticParamsinterceptor sets it before the parameters interceptor runs and it does not need@StrutsParameter. -
As named values (
{type}in the regex matcher,{id}in a namespace). In 7.4.0 these are appended to the static parameters of the mapping (struts.matcher.appendNamedParametersis true), so they also bind without an annotation.
Both ways are different from a query-string parameter, which binds only through a setter annotated with @StrutsParameter.
The scratch app proves the difference and a consequence.
public class UnannotatedBookAction extends ActionSupport {
private String type;
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public String getMessage() { return "type=" + type; }
}
g8wildrx<action name="/plain/{type}" class="org.example.bookshelf.UnannotatedBookAction">
<result>/WEB-INF/views/book.jsp</result>
</action>
$ curl -s 'http://localhost:18080/g8wildrx/plain/fiction?type=evil'
method=type=fiction type=fiction
$ curl -s 'http://localhost:18080/g8wildrx/fiction/content/Dune?type=evil'
method=list type=evil title=Dune
The first request shows that the path variable binds although the setter is not annotated, and that the query parameter type
does not replace it, because the setter is not annotated. The second shows the other side: the setter of BookCrudAction is
annotated, so a query parameter with the same name overrides the value that came from the path. Treat a path variable like any
other input. Validate it, and do not assume it is the value the route says it is.
A Class Chosen by a Wildcard and the OGNL Allowlist
A wildcard can build the class name, as in class="org.example.bookshelf.Edit{1}Action", and the default configuration
comment of Struts 7 still suggests <action name="*/*" method="{2}" class="actions.{1}">. In Struts 7 that mapping fails by
default. The OGNL allowlist (struts.allowlist.enable is on by default from 7.0) allows the classes that the configuration names,
and a class that appears only through a placeholder is not named anywhere. The framework calls execute() through OGNL and the
call is blocked.
/admin/editAuthor request in the default scratch app, where EditAuthorAction is reached only through {1}$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g8wild/admin/editAuthor
500
$ curl -s http://localhost:18080/g8wild/admin/editAuthor | grep -o "NoSuchMethodException[^<]*"
NoSuchMethodException: org.example.bookshelf.EditAuthorAction.execute()
EditBookAction in the same package worked because a different mapping (direct) names it. Adding the package to the allowlist
fixes the other class.
<constant name="struts.allowlist.packageNames" value="org.example.bookshelf"/>
g8wildal)$ curl -s http://localhost:18080/g8wildal/admin/editAuthor
EditAuthor view: EditAuthorAction ran, id=null
The failure shows as an HTTP 500 and a NoSuchMethodException for a method that exists. An application that logs at WARN level also shows a warning from SecurityMemberAccess that names the blocked class and recommends
struts.allowlist.classes or struts.allowlist.packageNames, which is the clue to read. The better design avoids the
problem: a mapping whose class comes from request text means that the request chooses which Java class to run, so allowlist a
small package of such classes and keep other classes out of it.
Wildcards in the Convention Plugin
The Convention plugin (see Convention Plugin & Annotations) builds mappings from annotations. A wildcard name in @Action is a wildcard mapping like any other, and
the plugin’s registration order has no relation to the source order of the classes. That was a bug until 7.3.0: a broad
pattern registered before a narrower one that it also matches could shadow it, in an order that differed between JVMs
(WW-3784). The sort rule of Which Pattern Wins is the fix, and 7.4.0 corrects the position of the ** key (WW-5743).
Pitfalls and Error Names
| Symptom or error | Cause | Fix |
|---|---|---|
404 |
The method that the placeholder produced is not allowed |
List the method in |
404 |
DMI is off, so the bang name is looked up literally |
Use a wildcard mapping, or turn DMI on knowingly |
404 |
DMI is on and SMI refuses the method |
Add the method to the action’s list, or stop using DMI |
HTTP 500 |
The name is on the global allowed list but the class has no such method |
Add the method, or remove the placeholder value that selects it |
HTTP 500 |
The class is chosen by |
|
Application does not start, "The content of element type action must match" (observed) |
|
Put |
Application does not start, |
A regex segment with braces such as |
Write the class without a repetition count |
The generic mapping answers for a URL that a specific mapping below it also matches (observed) |
The first matching pattern wins, contrary to the Core Developer Guide’s text |
Declare the specific pattern first and the catch-all last |
404 |
Slashes in action names are off, or the full namespace is selected |
Set the two constants of Slashes in Action Names |
|
The wildcard is not greedy and stops at the first |
Use |
|
Only one pattern matcher is active, and the regex matcher treats names without |
Convert the patterns, or keep the default matcher |
A getter runs as an action method and the page reports |
A package with SMI off and the default method regex |
Keep SMI on, or narrow |
A method intended for maintenance, such as |
A package with |
Delete the attribute and list the allowed methods |
A button with |
It renders |
Use separate forms or |
A path variable is replaced by a query parameter of the same name (observed) |
The setter is annotated, so the parameters interceptor runs after the static parameters |
Use a different property for the query parameter, or validate the final value |
|
A named-variable namespace and a tag that guesses the namespace |
Write the URL by hand or use a custom |
The 404 page shows the raw action name |
Struts does not escape action names in its automatically generated error pages |
Define a global error page, or disable DMI (S2-006 in the security guide) |
What Changed Recently
| Version | Change |
|---|---|
2.5 |
Strict method invocation replaces the older strict DMI (the Core Developer Guide dates the earlier strict DMI to 2.3, the security guide dates strict method invocation to 2.5); |
2.5.25 |
The regex matcher needs |
6.0 |
The |
7.0 |
The OGNL allowlist is on by default; |
7.3.0 |
Annotated wildcard actions are sorted most specific first before registration (WW-3784) |
7.4.0 |
The path-spanning |
Source: Struts 7.4.0 Version Notes
Source: Apache JIRA — WW-4405
Source: Apache JIRA — WW-3784
Source: Apache JIRA — WW-5743
Where the Books Are Outdated
Struts in Action (Manning, 2002, Struts 1) and Struts 2 in Action (Manning, 2008, Struts 2.0) are listed in the section bibliography. They teach the same problem, and the answers have moved.
| The book’s material | Today |
|---|---|
Struts 1 |
Both are gone with Struts 1, which has been end of life since 2013 and has unfixed vulnerabilities. The replacement is a wildcard mapping with |
The Struts 1 mapping path |
A name without a slash, the |
Dynamic method invocation |
Off by default, and every method it names must be allowed. The security guide and the tutorial recommend turning it off |
|
Both render prefixes that are off by default; use separate forms or |
|
|
Wildcard examples with |
The OGNL allowlist blocks the class unless the package is allowlisted |
No method allow list: every public method of an action was callable |
Strict method invocation, on by default since 2.5 |
References
-
Struts Core Developer Guide — Wildcard Mappings, Ordering of annotated wildcard actions
-
Struts Core Developer Guide — Wildcard Mappings, Parameters in namespaces
-
Struts Core Developer Guide — Wildcard Mappings, Parameters after the action name
-
Struts Core Developer Guide — Wildcard Mappings, Advanced Wildcards
-
Struts Core Developer Guide — Action Configuration, Action Methods
-
Struts Core Developer Guide — Action Configuration, Wildcard Method
-
Struts Core Developer Guide — Action Configuration, Dynamic Method Invocation
-
Struts Core Developer Guide — Action Configuration, Strict DMI
-
Struts Core Developer Guide — Action Configuration, Strict Method Invocation
-
Struts Core Developer Guide — Action Configuration, Action Default
-
Struts Getting Started — Wildcard Method Selection, Dynamic Method Invocation
-
Struts security guide — Defining and annotating your Action parameters
-
Struts in Action (Manning)
-
Struts 2 in Action (Manning)