[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[FD] Apache Struts2 Core and Plugins <=7.4.0 Multiple Null Pointer Exceptions



The Apache Struts2 Core, Apache Struts2 Convention Plugin, Apache Struts2 REST 
plugin, and the Apache Struts2 JavaTemplates plugin, Apache Struts2 Tiles 
plugin, etc., all versions <=7.4.0/<=6.12.0/etc., dereference or iterate over 
data that can be non-null -- such as an unmapped configuration lookup, an 
absent collection element, a failed backing-store lookup, an unselected 
optional upload field, or an unresolved expression-language property, a 
security pattern-matching call invoked with no candidate expression to test, or 
a missing bundle entry's substituted default message -- without first checking 
whether that value is `null`.

Impact
Any caller -- an attacker or an entirely ordinary user -- who executes a code 
path in which the underlying value is absent or `null` (e.g., an incomplete 
configuration hierarchy, a missing or empty form field, a non-existent lookup 
key, an unselected optional upload, or application data that simply does not 
expose the expected property) will cause an uncaught `NullPointerException` to 
bubble up the stack, resulting in a `500 Internal Server Error`.  This may 
result in information disclosure -- either directly or otherwise - and, like 
all `500` responses, signals to malicious entities that the information system 
contains significant bugs, errors, and unintended side effects brought about by 
unexpected input and edge cases.

While these may seem to merely be "reliability bugs", they undermine the 
availability of systems that rely upon the underlying code, especially if the 
victim is on behind a load balancer or other front-end system that sees a `500 
Internal Server Error` as a sign of an unhealthy environment and tears it down. 
 Moreover, it is not inconceivable that a configuration file or variable is 
non-null in development, staging, or elsewhere but not in production, resulting 
in unexpected exceptions and other issues that either were not identified 
during testing or could not have been identified in testing.  It should also be 
noted that these kinds of issues are examples of poor coding standards, 
testing, and weak peer-review processes.

Examples
As with many of the previously released advisories, there are many instances of 
this, so we will limit it to a subset of instances from which the others may be 
derived -- again, for the sake of brevity and because it simply isn't our job 
to enumerate all instances of a single vulnerable (like with the Struts2 
insecure JSON deserialisation pathways where we made clear the issue, 
mechanisms, and proof of concept while also citing sample responsible code and 
providing detailed analysis thereof).  Please excuse us for our terseness on 
this one -- we have written up many of these, as of late, and the rather 
glaring theme and sheer number of issues makes such write-ups time consuming 
(old code or not, all of these are things that have been well-documented, 
frowned upon coding paradigms and constructs for literal decades, long before 
any of the code in question was written) -- and this is all without getting 
into the more "juicy" findings.

RangeValidatorSupport -- Null Elements
`RangeValidatorSupport.validate(Object)`, in `struts2-core`, only `null`-checks 
the top-level array- or `Collection`-typed field before iterating over the 
elements and passing each one to `validateValue(Object, Comparable, T, T)`, 
which can call to `value.compareTo(min)` and `value.compareTo(max)` -- so a 
single `null` element inside an otherwise non-`null` collection will cause a 
`NullPointerException`.

```java
public void validate(Object object) throws ValidationException {
    Object obj = getFieldValue(getFieldName(), object);

    // if there is no value - don't do comparison
    // if a value is required, a required validator should be added to the field
    if (obj == null) {
        return;
    }

    T min = getMin();
    T max = getMax();

    if (obj.getClass().isArray()) {
        Object[] values = (Object[]) obj;
        for (Object objValue : values) {
            validateValue(object, (Comparable<T>) objValue, min, max);
        }
    } else if (Collection.class.isAssignableFrom(obj.getClass())) {
        Collection<?> values = (Collection<?>) obj;
        for (Object objValue : values) {
            validateValue(object, (Comparable<T>) objValue, min, max);
        }
    } else {
        validateValue(object, (Comparable<T>) obj, min, max);
    }
}
```
Code snippet from `struts2-core`'s 
core/src/main/java/org/apache/struts2/validator/validators/RangeValidatorSupport.java,
 lines 46-71

```java
protected void validateValue(Object object, Comparable<T> value, T min, T max) {
    setCurrentValue(value);

    // only check for a minimum value if the min parameter is set
    if ((min != null) && (value.compareTo(min) < 0)) {
        addFieldError(getFieldName(), object);
    }

    // only check for a maximum value if the max parameter is set
    if ((max != null) && (value.compareTo(max) > 0)) {
        addFieldError(getFieldName(), object);
    }

    setCurrentValue(null);
}
```
Snippet from `struts2-core`'s 
core/src/main/java/org/apache/struts2/validator/validators/RangeValidatorSupport.java,
 lines 73-87, showing the unconditional `value.compareTo` dereference

DoubleRangeValidator -- Null Elements
`DoubleRangeFieldValidator`, in `struts2-core`, does not extend 
`RangeValidatorSupport<T>` the way that `IntRangeFieldValidator`, 
`LongRangeFieldValidator`, `ShortRangeFieldValidator`, and 
`LocalDateRangeFieldValidator` all do -- it implements its own 
`validate(Object)`, `validateCollection(Double, Double, Double, Double, 
Collection)`, and `validateValue(Object, Double, Double, Double, Double)` 
methods -- and contains the identical problematic code but under the `validate` 
to `validateCollection` pathway, which, again, only performs a nullness check 
on the top-level object, but, this time, the exception occurs when 
`validateValue` unconditionally calls `Object.toString()` inside of a try-catch 
block that only handles `NumberFormatException`.

```java
    public void validate(Object object) throws ValidationException {
        String fieldName = getFieldName();
        Object obj = this.getFieldValue(fieldName, object);
        if (obj == null) {
            return;
        }

        Double maxInclusiveToUse = getMaxInclusive();
        Double minInclusiveToUse = getMinInclusive();
        Double maxExclusiveToUse = getMaxExclusive();
        Double minExclusiveToUse = getMinExclusive();

        if (obj.getClass().isArray()) {
            Object[] values = (Object[]) obj;
            validateCollection(maxInclusiveToUse, minInclusiveToUse, 
maxExclusiveToUse, minExclusiveToUse, Arrays.asList(values));
        } else if (Collection.class.isAssignableFrom(obj.getClass())) {
            Collection values = (Collection) obj;
            validateCollection(maxInclusiveToUse, minInclusiveToUse, 
maxExclusiveToUse, minExclusiveToUse, values);
        } else {
            validateValue(obj, maxInclusiveToUse, minInclusiveToUse, 
maxExclusiveToUse, minExclusiveToUse);
        }
    }

    protected void validateCollection(Double maxInclusiveToUse, Double 
minInclusiveToUse, Double maxExclusiveToUse, Double minExclusiveToUse, 
Collection values) {
        for (Object objValue : values) {
            validateValue(objValue, maxInclusiveToUse, minInclusiveToUse, 
maxExclusiveToUse, minExclusiveToUse);
        }
    }
```
Code snippet from `struts2-core`'s 
core/src/main/java/org/apache/struts2/validator/validators/DoubleRangeFieldValidator.java,
 lines 109-136, showing the top-level-only null check at line 112 and 
`validateCollection()`'s per-element iteration with no null check

```java
    protected void validateValue(Object obj, Double maxInclusiveToUse, Double 
minInclusiveToUse, Double maxExclusiveToUse, Double minExclusiveToUse) {
        try {
            setCurrentValue(obj);
            Double value = Double.valueOf(obj.toString());
            if ((maxInclusiveToUse != null && 
value.compareTo(maxInclusiveToUse) > 0) ||
                    (minInclusiveToUse != null && 
value.compareTo(minInclusiveToUse) < 0) ||
                    (maxExclusiveToUse != null && 
value.compareTo(maxExclusiveToUse) >= 0) ||
                    (minExclusiveToUse != null && 
value.compareTo(minExclusiveToUse) <= 0)) {
                addFieldError(getFieldName(), value);
            }
        } catch (NumberFormatException e) {
            LOG.debug("Cannot validate value {} - not a Double", obj, e);
        } finally {
            setCurrentValue(null);
        }
    }
```
Snippet from `struts2-core`'s 
core/src/main/java/org/apache/struts2/validator/validators/DoubleRangeFieldValidator.java,
 lines 138-153, showing the unconditional `obj.toString()` call and the `catch` 
clause that handles only `NumberFormatException`

FileUploadAction/MultipleFileUploadUsingArrayAction -- Nullable Optional Upload
`FileUploadAction.getUploadedFile()` and 
`MultipleFileUploadUsingArrayAction.upload()`, both in `struts2-showcase`, 
dereference or iterate over their respective `uploadedFile`/`uploadedFiles` 
fields without a nullness check, but `ActionFileUploadInterceptor`, in 
`struts2-core`, only ever assigns either field when at least one file was 
accepted from the multipart request -- leaving both fields `null` whenever the 
form is submitted without a file having been selected.  Importantly, 
`struts2-showcase` is "sample code" and contains cross-site scripting (XSS) and 
various other issues (OGNL injection, of course), but we felt it important to 
show the code so that others are aware of how the interceptor operates and to 
not use the sample code as any kind of a guide (for many reasons).

```java
    public Object getUploadedFile() {
        return uploadedFile.getContent();
    }
```
Code snippet from `struts2-showcase`'s 
apps/showcase/src/main/java/org/apache/struts2/showcase/fileupload/FileUploadAction.java,
 lines 66-68

```java
    public String upload() throws Exception {
        System.out.println("\n\n upload2");
        System.out.println("files:");
        for (UploadedFile u : uploadedFiles) {
            System.out.println("*** " + u + "\t" + u.length());
        }
```
Snippet from `struts2-showcase`'s 
apps/showcase/src/main/java/org/apache/struts2/showcase/fileupload/MultipleFileUploadUsingArrayAction.java,
 lines 41-45

```java
        if (acceptedFiles.isEmpty()) {
            LOG.debug("No files have been uploaded/accepted");
        } else {
            LOG.debug("Passing: {} uploaded file(s) to action", 
acceptedFiles.size());
            action.withUploadedFiles(acceptedFiles);
        }

        // invoke action
        return invocation.invoke();
```
Snippet from `struts2-core`'s 
core/src/main/java/org/apache/struts2/interceptor/ActionFileUploadInterceptor.java,
 lines 278-286, showing both fields left unset whenever no files are accepted

SelectHandle -- Option Group Rendering Dereferences an Unresolved Lookup
`SelectHandler.writeOptionGroup()`, in `struts2-javatemplates-plugin`, calls 
`toString()` directly on the object returned by a value-stack lookup for a 
configured list-key or list-value property without a nullness check, even 
though the sibling top-level list-rendering code in the very same class already 
null-checks the identical lookup before calling `toString()` on it.

```java
                Object tmpKey = stack.findValue(listKey != null ? listKey : 
"top");
                String tmpKeyStr = StringUtils.defaultString(tmpKey.toString());
                Object tmpValue = stack.findValue(listValue != null ? listValue 
: "top");
                String tmpValueStr = 
StringUtils.defaultString(tmpValue.toString());
```
Code snippet from `struts2-javatemplates-plugin`'s 
plugins/javatemplates/src/main/java/org/apache/struts2/views/java/simple/SelectHandler.java,
 lines 135-138

```java
                //key
                Object itemKey = findValue(listKey != null ? listKey : "top");
                String itemKeyStr = StringUtils.defaultString(itemKey == null ? 
null : itemKey.toString());
                //value
                Object itemValue = findValue(listValue != null ? listValue : 
"top");
                String itemValueStr = StringUtils.defaultString(itemValue == 
null ? null : itemValue.toString());
```
Snippet from `struts2-javatemplates-plugin`'s 
plugins/javatemplates/src/main/java/org/apache/struts2/views/java/simple/SelectHandler.java,
 lines 82-87, showing the sibling null-guarded equivalent lookup

Accepted/Excluded Patterns Checker -- Unguarded Regex Match Against a Null 
Expression
`DefaultNotExcludedAcceptedPatternsChecker.isAllowed(String)`, 
`isAccepted(String)`, and `isExcluded(String)`, in `struts2-core`, forward 
their `value` parameter to `DefaultAcceptedPatternsChecker.isAccepted(String)` 
and `DefaultExcludedPatternsChecker.isExcluded(String)`, both of which call 
`pattern.matcher(value)` for every configured pattern without a nullness check.

```java
    @Override
    public IsAllowed isAllowed(String value) {
        IsExcluded isExcluded = isExcluded(value);
        if (isExcluded.isExcluded()) {
            return IsAllowed.no(isExcluded.getExcludedPattern());
        }

        IsAccepted isAccepted = isAccepted(value);
        if (!isAccepted.isAccepted()) {
            return IsAllowed.no(isAccepted.getAcceptedPattern());
        }

        return IsAllowed.yes(isAccepted.getAcceptedPattern());
    }

    @Override
    public IsAccepted isAccepted(String value) {
        return acceptedPatterns.isAccepted(value);
    }
```
Code snippet from `struts2-core`'s 
core/src/main/java/org/apache/struts2/security/DefaultNotExcludedAcceptedPatternsChecker.java,
 lines 41-59, showing `isAllowed(String)` and `isAccepted(String)` forwarding 
`value` onward without protecting against `null`s

```java
    @Override
    public IsExcluded isExcluded(String value) {
        return excludedPatterns.isExcluded(value);
    }
```
Snippet from `struts2-core`'s 
core/src/main/java/org/apache/struts2/security/DefaultNotExcludedAcceptedPatternsChecker.java,
 lines 81-84, showing `isExcluded(String)`similarly  forwarding `value`

```java
    @Override
    public IsAccepted isAccepted(String value) {
        for (Pattern acceptedPattern : acceptedPatterns) {
            if (acceptedPattern.matcher(value).matches()) {
                LOG.trace("[{}] matches accepted pattern [{}]", value, 
acceptedPattern);
                return IsAccepted.yes(acceptedPattern.toString());
            }
        }
        return IsAccepted.no(acceptedPatterns.toString());
    }
```
Snippet from `struts2-core`'s 
core/src/main/java/org/apache/struts2/security/DefaultAcceptedPatternsChecker.java,
 lines 122-131, showing `isAccepted(String)`'s unconditional 
`pattern.matcher(value)` call

```java
    @Override
    public IsExcluded isExcluded(String value) {
        for (Pattern excludedPattern : excludedPatterns) {
            if (excludedPattern.matcher(value).matches()) {
                LOG.trace("[{}] matches excluded pattern [{}]", value, 
excludedPattern);
                return IsExcluded.yes(excludedPattern);
            }
        }
        return IsExcluded.no(excludedPatterns);
    }
```
Snippet from `struts2-core`'s 
core/src/main/java/org/apache/struts2/security/DefaultExcludedPatternsChecker.java,
 lines 106-115, showing `isExcluded(String)`'s identical behaviour

DefaultTextProvider -- Null Default Value Omitted From the String-Array-Args 
Overload's Null Guard
`DefaultTextProvider`'s `List<?>`-args overload of `getText(String, String, 
List<?>)`, in `struts2-core`, explicitly substitutes `key` for `defaultValue` 
whenever both the bundle lookup and the caller-supplied default are `null`, so 
`new MessageFormat()` never receives a `null` pattern, but the 
otherwise-parallel `String[]`-args overload omits that same check entirely, so 
an identical bundle miss combined with a `null` `defaultValue` reaches `new 
MessageFormat(defaultValue)`, after which an exception will be thrown.

```java
    @Override
    public String getText(String key, String defaultValue, List<?> args) {
        String text = getText(key, args);
        if(text == null && defaultValue == null) {
            defaultValue = key;
        }
        if (text == null && defaultValue != null) {

            MessageFormat format = new MessageFormat(defaultValue);
            format.setLocale(ActionContext.getContext().getLocale());
            format.applyPattern(defaultValue);

            Object[] params;
            if (args != null) {
                params = args.toArray();
            } else {
                params = EMPTY_ARGS;
            }

            return format.format(params);
        }
        return text;
    }
```
Code snippet from `struts2-core`'s 
core/src/main/java/org/apache/struts2/text/DefaultTextProvider.java, lines 
91-113, showing the `List<?>`-args overload's `text == null && defaultValue == 
null` substitution

```java
    @Override
    public String getText(String key, String defaultValue, String[] args) {
        String text = getText(key, args);
        if (text == null) {
            MessageFormat format = new MessageFormat(defaultValue);
            format.setLocale(ActionContext.getContext().getLocale());
            format.applyPattern(defaultValue);

            return format.format(requireNonNullElse(args, EMPTY_ARGS));

        }
        return text;
    }
```
Snippet from `struts2-core`'s 
core/src/main/java/org/apache/struts2/text/DefaultTextProvider.java, lines 
115-127, showing the sibling `String[]`-args overload reaching `new 
MessageFormat(defaultValue)` without an equivalent guard

Steps to Reproduce
The following are various snippets, etc., that will allow you to reproduce some 
of the above.  You can easily make sample Apache Struts2 applications that 
prove each of these, but, for the sake of brevity, we will show you two harness 
runs against the code, directly.  Enjoy.

Range Validator Instance
```java
static void runCase(String label, Integer[] values) {
    ArrayFieldStub validator = new ArrayFieldStub(values);
    validator.setMin(1);
    validator.setMax(100);
    try {
        validator.validate(new Object());
        System.out.println(label + ":NO_EXCEPTION");
    } catch (Throwable e) {
        StackTraceElement top = e.getStackTrace().length > 0 ? 
e.getStackTrace()[0] : null;
        System.out.println(label + ":EXCEPTION:" + e.getClass().getName() + ":" 
+ e.getMessage()
            + ":TOP_FRAME:" + (top != null ? top.getClassName() + "." + 
top.getMethodName() + ":" + top.getLineNumber() : "none"));
    }
}
```
Excerpt from the probe, running the NULL_ELEMENT case (stub array {5, null, 
15}) against the ALL_VALID negative control ({5, 8, 9}), both with min=1/max=100

```text
VULNERABLE -- an Integer[] containing one null element ({5, null, 15}) with 
min=1/max=100 configured threw
an uncaught java.lang.NullPointerException whose top stack frame is
'org.apache.struts2.validator.validators.RangeValidatorSupport.validateValue:77'
while the byte-identical in-range, null-free array {5, 8, 9} returned normally 
without an exception
```
A run of the harness shows it vulnerable with the control passing and the test 
failing

Double Range Validator Instance
```java
public static void main(String[] args) {
    runCase("NULL_ELEMENT_ARRAY", new Object[]{"5.0", null, "15.0"});
    runCase("ALL_VALID_ARRAY", new Object[]{"5.0", "8.0", "9.0"});
    runCase("EMPTY_STRING_ELEMENT_ARRAY", new Object[]{"5.0", "", "9.0"});
    runCase("NULL_ELEMENT_COLLECTION", Arrays.asList("5.0", null, "15.0"));
    runCase("ALL_VALID_COLLECTION", Arrays.asList("5.0", "8.0", "9.0"));
}
```
Excerpt from the probe's `main()` method, exercising the array branch 
(`NULL_ELEMENT_ARRAY` against the `ALL_VALID_ARRAY` and 
`EMPTY_STRING_ELEMENT_ARRAY` controls) and the `Collection` branch 
(`NULL_ELEMENT_COLLECTION` against the `ALL_VALID_COLLECTION` control), all 
with `minInclusive=1.0`/`maxInclusive=100.0`

```text
NULL_ELEMENT_ARRAY:EXCEPTION:java.lang.NullPointerException:Cannot invoke 
"Object.toString()" because "obj" is 
null:TOP_FRAME:org.apache.struts2.validator.validators.DoubleRangeFieldValidator.validateValue:141
ALL_VALID_ARRAY:NO_EXCEPTION
EMPTY_STRING_ELEMENT_ARRAY:NO_EXCEPTION
NULL_ELEMENT_COLLECTION:EXCEPTION:java.lang.NullPointerException:Cannot invoke 
"Object.toString()" because "obj" is 
null:TOP_FRAME:org.apache.struts2.validator.validators.DoubleRangeFieldValidator.validateValue:141
ALL_VALID_COLLECTION:NO_EXCEPTION
```
A run of the harness shows it vulnerable with the control passing and the test 
failing
_______________________________________________
Sent through the Full Disclosure mailing list
https://nmap.org/mailman/listinfo/fulldisclosure
Web Archives & RSS: https://seclists.org/fulldisclosure/