Lukasz Lenart created WW-5700:
---------------------------------

             Summary: Failed type conversion stores the NO_CONVERSION_POSSIBLE 
marker string into typed Maps, Lists and Collections
                 Key: WW-5700
                 URL: https://issues.apache.org/jira/browse/WW-5700
             Project: Struts 2
          Issue Type: Bug
            Reporter: Lukasz Lenart


h2. Summary

When conversion of a request parameter into a typed collection fails, Struts 
stores its internal
"conversion failed" marker into the collection instead of skipping the 
assignment. The marker is
itself a {{java.lang.String}}, so it lands in a collection declared to hold 
some other type. Because
generics are erased at that point the store succeeds silently, and the 
{{ClassCastException}} is
deferred until application code reads the entry back.

The resulting stack trace points at _application_ code rather than at Struts, 
which makes this very
hard to recognise from a bug report.

Reported on the user list:
"Struts setting a String object instead of Integer in the form" (2026-08-14), 
against 7.2.1 and 7.3.0.

h2. Root cause

{{TypeConverter.NO\_CONVERSION\_POSSIBLE}} is not a sentinel object. It is a 
plain String:

{code:java}
// core/src/main/java/org/apache/struts2/conversion/TypeConverter.java:49
Object NO\_CONVERSION\_POSSIBLE = "ognl.NoConversionPossible";
{code}

{{XWorkConverter.convertValue}} returns it on failure (lines 339, 351, 361), 
_after_ correctly
registering the conversion error via {{handleConversionException}}. Three 
property accessors then
store that return value with no guard:
* {{XWorkMapPropertyAccessor.setProperty}} (~line 127) - both {{getKey()}} and 
{{getValue()}} unguarded
* {{XWorkListPropertyAccessor.getRealValue}} (line 188)
* {{XWorkCollectionPropertyAccessor.getRealValue}} (line 263)

By contrast OGNL itself guards this correctly at {{OgnlRuntime}} line 1323, 
which is why a plain
(non-collection) property is left untouched on a failed conversion.

h2. Reproduced

On {{main}} at 05ad78a06, end-to-end through {{ParametersInterceptor}} with
{{struts.parameters.requireAnnotations=true}}. Both halves reproduce.

_Value half_ - the reporter's case. An unchecked {{s:checkbox}} with 
{{submitUnchecked="true"}}
causes {{CheckboxInterceptor}} to submit the parameter with its 
{{uncheckedValue}}, default
{{"false"}}. Bound into a HashMap with Long keys and Integer values:

{noformat}
key=100 value=[1] (java.lang.Integer)
key=200 value=[ognl.NoConversionPossible] (java.lang.String)
conversionErrors={capDeferral[200]=ConversionData@...}
{noformat}

_Key half_ - the marker is stored as a map _key_, which breaks iteration over 
the entire map rather
than a single entry:

{noformat}
acceptable=[capDeferral['abc'], capDeferral[7]]
key=[ognl.NoConversionPossible] (java.lang.String) value=[1]
key=[7] (java.lang.Long) value=[2]
conversionErrors={capDeferral['abc']=ConversionData@...}
{noformat}

The key half is reachable because {{ACCEPTED\_PATTERNS}} in
{{DefaultAcceptedPatternsChecker}} is asymmetric: the bare-bracket branch 
accepts digits only,
{{(\[\d+])}}, but the quoted-key branch accepts word characters,
{{(\['(\w-?|[一-龥]-?)+'])}}. So {{capDeferral['abc']}} is an accepted parameter 
name even
where the declared map key type is numeric, and nothing downstream re-checks 
the key against that
type. This is worth stating explicitly because the natural "map indices are 
numeric" intuition does
not hold.

Note the conversion error _is_ reported in both halves. This is therefore not a 
validation bypass:
it only bites an action that reads the collection without acting on conversion 
errors.

h2. Proposed fix

Guard for the marker and skip the store; the error has already been registered, 
so nothing is lost.

{code:java}
Object key = getKey(context, name);
if (key == TypeConverter.NO\_CONVERSION\_POSSIBLE) {
    return;
}
Object convertedValue = getValue(context, value);
if (convertedValue == TypeConverter.NO\_CONVERSION\_POSSIBLE) {
    return;
}
map.put(key, convertedValue);
{code}

Same guard in the List and Collection accessors, placed immediately after 
{{getRealValue}} and, in
the List case, _before_ the auto-grow block so an unconvertible value does not 
grow the list.

Use identity comparison rather than {{equals}}, matching OGNL's own guard: the 
constant is a String
literal and the converter returns that exact reference, so with {{equals}} a 
form legitimately
submitting the text "ognl.NoConversionPossible" into a String-valued map would 
be silently
discarded.

h2. Precedent

WW-3762 fixed exactly this class of bug in 
{{XWorkBasicConverter.doConvertToCollection}} back in
2.3.3. {{CollectionConverter}} still carries that guard today, three times. The 
property accessors
were simply never given the equivalent check.

h2. Backward compatibility

Narrow. Previously an unconvertible entry was stored as the marker String; now 
nothing is stored.
Nothing can reasonably depend on the old behaviour, and the conversion error is 
reported either way,
so validation-driven actions see no change at all.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to