WW-5700 fix(ognl): skip the store when a map or list element cannot be converted - #1873
WW-5700 fix(ognl): skip the store when a map or list element cannot be converted#1873lukaszlenart wants to merge 1 commit into
Conversation
…e converted XWorkConverter.convertValue() signals failure by returning TypeConverter.NO_CONVERSION_POSSIBLE, which is itself a plain String, "ognl.NoConversionPossible". XWorkMapPropertyAccessor and XWorkListPropertyAccessor stored that return value into the target collection without checking for it. Because generics are erased at that point the store succeeds silently, so the ClassCastException surfaces later, in application code reading the entry back, with a stack trace that points away from the framework. Guard both accessors and skip the assignment instead; the conversion error has already been registered by convertValue(), so nothing is lost. The map accessor guards the key as well as the value - a key that cannot be converted poisons iteration over the whole map, not one entry. Identity comparison is used rather than equals(), matching OGNL's own guard in OgnlRuntime, so a form legitimately submitting the literal text "ognl.NoConversionPossible" into a String-valued collection is not silently discarded. In the list accessor the guard sits before the auto-grow block so an unconvertible value does not grow the list. XWorkCollectionPropertyAccessor carries the same unguarded pattern but is left untouched: its scalar setProperty path is not reachable through the value stack, so no failing test could be written for it. Reported on user@ as "Struts setting a String object instead of Integer in the form", where an unchecked s:checkbox with submitUnchecked="true" submits the CheckboxInterceptor uncheckedValue "false" into a Map<Long, Integer>. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes a type-conversion edge case in Struts’ OGNL property accessors where XWorkConverter.convertValue() can return TypeConverter.NO_CONVERSION_POSSIBLE, and that sentinel value was previously being stored into Maps/Lists during parameter binding. The change prevents silent poisoning of typed collections (and delayed ClassCastExceptions) by skipping the assignment when conversion fails, with new regression tests covering the reported checkbox scenario and direct accessor behavior.
Changes:
- Add guards in
XWorkMapPropertyAccessorto skipmap.put(...)when either the converted key or converted value isTypeConverter.NO_CONVERSION_POSSIBLE. - Add a guard in
XWorkListPropertyAccessorto skiplist[index]=...(and avoid auto-grow) when the converted element isTypeConverter.NO_CONVERSION_POSSIBLE. - Add test coverage for unconvertible key/value/element cases, including an end-to-end
ParametersInterceptorbinding test reproducing WW-5700.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| core/src/main/java/org/apache/struts2/ognl/accessor/XWorkMapPropertyAccessor.java | Prevents storing the “no conversion possible” sentinel into typed Maps by skipping assignment when key/value conversion fails. |
| core/src/main/java/org/apache/struts2/ognl/accessor/XWorkListPropertyAccessor.java | Prevents storing the sentinel into typed Lists (and avoids list auto-growth) when element conversion fails. |
| core/src/test/java/org/apache/struts2/ognl/accessor/XWorkMapPropertyAccessorTest.java | Adds regression tests ensuring unconvertible Map keys/values are not stored into typed Maps. |
| core/src/test/java/org/apache/struts2/ognl/accessor/XWorkListPropertyAccessorTest.java | Adds regression test ensuring an unconvertible List element is not stored into a typed List. |
| core/src/test/java/org/apache/struts2/interceptor/parameter/ParametersInterceptorTest.java | Adds an end-to-end regression test for WW-5700 reproducing the checkbox “false” → Integer conversion failure scenario. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|



Fixes WW-5700
Problem
XWorkConverter.convertValue()signals a failed conversion by returningTypeConverter.NO_CONVERSION_POSSIBLE— which is itself a plainString,"ognl.NoConversionPossible".XWorkMapPropertyAccessorandXWorkListPropertyAccessorstored that return value into the target collectionwithout checking for it.
Generics are erased at that point, so the
putsucceeds silently and theClassCastExceptiononly surfaces later, when application code reads the entryback — with a stack trace pointing away from the framework, which makes this
very hard to recognise from a bug report.
Reported on user@ as "Struts setting a String object instead of Integer in the
form": an unchecked
<s:checkbox submitUnchecked="true"/>causesCheckboxInterceptorto submit itsuncheckedValue,"false", which cannotbecome an
Integer.Fix
Guard both accessors and skip the assignment. The conversion error has already
been registered by
convertValue(), so nothing is lost — validation-drivenactions see no change at all.
Details worth a reviewer's attention:
The map accessor guards the key as well as the value. An unconvertible key
poisons iteration over the whole map rather than a single entry, and is
reachable because
ACCEPTED_PATTERNSis asymmetric: the bare-bracket branchaccepts digits only,
(\[\d+]), but the quoted-key branch accepts wordcharacters,
(\['(\w-?|[一-龥]-?)+']). SocapDeferral['abc']is an acceptedparameter name even where the declared key type is numeric.
Identity comparison, not
equals(), matching OGNL's own guard inOgnlRuntime. The constant is declaredObject, notString(
TypeConverter.java:49), so it is not a JLS constant variable and is notinlined into referencing class files — every reference resolves to the one field
value at runtime. A parameter value built by a servlet container from request
bytes is a distinct object, so reference comparison separates "the converter
signalled failure" from "the user submitted this text". Please do not
simplify this back to
equals().To be precise about the limit: this protects values arriving from a request,
which is the case that matters here. It does not protect a value that
happens to be interned — application code calling the converter programmatically
with a String literal shares the constant's exact instance, since all identical
literals are interned together. Closing that as well would mean giving the
marker an identity no user string can share, which changes a published constant
and is a binary-compatibility question rather than a bug fix. Out of scope here,
and noted so the limit is not misread as an oversight.
The guard skips rather than throws: on the
XWorkMethodAccessor.callMethodpath an exception here would be swallowed and only logged.
In the list accessor the guard sits before the auto-grow block, so an
unconvertible value does not grow the list.
Not included
XWorkCollectionPropertyAccessorcarries the same unguarded pattern, but itsscalar
setPropertypath is not reachable through the value stack —ids[0]ona
Setis rejected by OGNL before it gets there. No failing test could bewritten, so it is deliberately left untouched rather than changed blind.
Precedent
WW-3762 fixed this same bug class
in
XWorkBasicConverter.doConvertToCollectionin 2.3.3, andCollectionConverterstill carries that guard today, three times. The property accessors were simply
never given the equivalent check.
Testing
Written test-first; each test was watched failing for the right reason before the
fix, with a valid entry binding first so none can pass vacuously. For the
end-to-end test the production change was stashed to confirm it fails without it.
XWorkMapPropertyAccessorTest— unconvertible value, unconvertible keyXWorkListPropertyAccessorTest— unconvertible elementParametersInterceptorTest— the reported checkbox scenario, end to endFull core suite: 3201 tests, 0 failures.
🤖 Generated with Claude Code