CVE-2022-21700 (Java, published 2022-01-18T23:15Z)
Description: value: Micronaut is a JVM-based, full stack Java framework designed for building JVM web applications with support for Java, Kotlin and the Groovy language. In affected versions sending an invalid Content Type header leads to memory leak in DefaultArgumentConversionContext as this type is erroneously used in static state. ### Impact Sending an invalid Content Type header leads to memory leak in `DefaultArgumentConversionContext` as this type is erroneously used in static state. ### Patches The problem is patched in Micronaut 3.2.7 and above. ### Workarounds The default content type binder can be replaced in an existing Micronaut application to mitigate the issue: ```java package example; import java.util.List; import io.micronaut.context.annotation.Replaces; import io.micronaut.core.convert.ConversionService; import io.micronaut.http.MediaType; import io.micronaut.http.bind.DefaultRequestBinderRegistry; import io.micronaut.http.bind.binders.RequestArgumentBinder; import jakarta.inject.Singleton; @Singleton @Replaces(DefaultRequestBinderRegistry.class) class FixedRequestBinderRegistry extends DefaultRequestBinderRegistry { public FixedRequestBinderRegistry(ConversionService conversionService, List<RequestArgumentBinder> binders) { super(conversionService, binders); } @Override protected void registerDefaultConverters(ConversionService<?> conversionService) { super.registerDefaultConverters(conversionService); conversionService.addConverter(CharSequence.class, MediaType.class, charSequence -> { try { return MediaType.of(charSequence); } catch (IllegalArgumentException e) { return null; } }); } } ``` ### References Commit that introduced the vulnerability https://github.com/micronaut-projects/micronaut-core/commit/b8ec32c311689667c69ae7d9f9c3b3a8abc96fe3 ### For more information If you have any questions or comments about this advisory: * Open an issue in [Micronaut Core](https://github.com/micronaut-projects/micronaut-core/issues) * Email us at [info@micronaut.io](mailto:info@micronaut.io)
Severity: MEDIUM (CVSS v3 base score: 5.3)
Weakness: CWE-400 \u2014 Uncontrolled Resource Consumption
The product does not properly control the allocation and maintenance of a limited resource, thereby enabling an actor to influence the amount of resources consumed, eventually leading to the exhaustion of available resources.
Repository: https://github.com/micronaut-projects/micronaut-core
Files changed:
core/src/main/java/io/micronaut/core/convert/ConversionContext.java
core/src/main/java/io/micronaut/core/convert/DefaultArgumentConversionContext.java
core/src/main/java/io/micronaut/core/convert/value/ConvertibleMultiValues.java
core/src/main/java/io/micronaut/core/convert/value/ConvertibleValues.java
core/src/main/java/io/micronaut/core/type/Argument.java
core/src/main/java/io/micronaut/core/type/DefaultArgument.java
http-client/src/main/java/io/micronaut/http/client/exceptions/HttpClientErrorDecoder.java
http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java
http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/CompletableFutureBodyBinder.java
http-server-netty/src/main/java/io/micronaut/http/server/netty/binders/PublisherBodyBinder.java
http-server-netty/src/test/groovy/io/micronaut/http/server/netty/cors/CorsFilterSpec.groovy
http-server/src/main/java/io/micronaut/http/server/cors/CorsFilter.java
http-server/src/main/java/io/micronaut/http/server/cors/CorsOriginConverter.java
http/src/main/java/io/micronaut/http/HttpHeaders.java
http/src/main/java/io/micronaut/http/MediaType.java
runtime/src/main/java/io/micronaut/cache/interceptor/CacheInterceptor.java
runtime/src/main/java/io/micronaut/scheduling/io/watch/FileWatchCondition.java
session/src/main/java/io/micronaut/session/binder/SessionArgumentBinder.java
Commit message: Use ConversionContext constants where possible instead of class (#2356)

Changes
-------
 * Added ArgumentConversionContext constants in ConversionContext
 * Replaced Argument.of and use of argument classes with
ConversionContext constants where possible
 * Added getFirst method in ConvertibleMultiValues that accepts
ArgumentConversionContent parameter

Partially addresses issue #2355

Vulnerable code:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
        return get(name, Object.class).isPresent();
    Argument INT = Argument.of(int.class);
    Argument LONG = Argument.of(long.class);
    Argument FLOAT = Argument.of(float.class);
    Argument DOUBLE = Argument.of(double.class);
    Argument VOID = Argument.of(void.class);
    Argument BYTE = Argument.of(byte.class);
    Argument BOOLEAN = Argument.of(boolean.class);
    Argument CHAR = Argument.of(char.class);
    Argument SHORT = Argument.of(short.class);
        this.type = type;
        this.name = name;
        this.annotationMetadata = annotationMetadata != null ? annotationMetadata : AnnotationMetadata.EMPTY_METADATA;
        this.typeParameters = initializeTypeParameters(genericTypes);
        this.typeParameterArray = genericTypes;
    private Map<String, Argument<?>> initializeTypeParameters(Argument[] genericTypes) {
            return Argument.of(String.class);
                                    typeVariable = Argument.of(PartData.class);
                                        typeVariable = Argument.of(PartData.class);
        return Argument.of(CompletableFuture.class);
        return Argument.of(Publisher.class);
        0 * headers.get(ACCESS_CONTROL_REQUEST_HEADERS, Argument.of(List,String))
        1 * headers.getFirst(ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.class) >> Optional.of(HttpMethod.GET)
        1 * headers.get(ACCESS_CONTROL_REQUEST_HEADERS, Argument.of(List,String)) >> ['foo', 'bar']
        2 * headers.getFirst(ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.class) >> Optional.of(HttpMethod.GET)
        2 * headers.get(ACCESS_CONTROL_REQUEST_HEADERS, Argument.of(List,String)) >> Optional.of(['foo'])
        2 * headers.get(ACCESS_CONTROL_REQUEST_HEADERS, Argument.of(List,String)) >> Optional.of(['X-Header', 'Y-Header'])
        1 * headers.getFirst(ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.class) >> Optional.of(HttpMethod.GET)
        2 * headers.get(ACCESS_CONTROL_REQUEST_HEADERS, Argument.of(List,String)) >> Optional.of(['X-Header', 'Y-Header'])
        1 * headers.getFirst(ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.class) >> Optional.of(HttpMethod.GET)
import io.micronaut.core.type.Argument;
                    Optional<HttpMethod> result = headers.getFirst(ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.class);
                    Optional<List> allowedHeaders = headers.get(ACCESS_CONTROL_REQUEST_HEADERS, Argument.of(List.class, String.class));
                    HttpMethod methodToMatch = preflight ? headers.getFirst(ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.class).orElse(requestMethod) : requestMethod;
                    Optional<List<String>> accessControlHeaders = headers.get(ACCESS_CONTROL_REQUEST_HEADERS, Argument.listOf(String.class));
                .get(ALLOWED_ORIGINS, Argument.listOf(String.class))
                .get(ALLOWED_METHODS, Argument.listOf(HttpMethod.class))
                .get(ALLOWED_HEADERS, Argument.listOf(String.class))
                .get(EXPOSED_HEADERS, Argument.listOf(String.class))
                .get(ALLOW_CREDENTIALS, Boolean.class)
                .get(MAX_AGE, Long.class)
        return get(name, Integer.class);
        return getFirst(name, String.class);
        return getFirst(HttpHeaders.CONTENT_TYPE, MediaType.class);
        Optional<Long> optional = getFirst(HttpHeaders.CONTENT_LENGTH, Long.class);
            .flatMap(s -> ConversionService.SHARED.convert(s, MediaType.class).map(Stream::of).orElse(Stream.empty()))
        return getFirst(CONNECTION, String.class).map(val -> val.equalsIgnoreCase("keep-alive")).orElse(false);
            Argument<?> firstTypeVariable = returnTypeObject.getFirstTypeVariable().orElse(Argument.of(Object.class));
        Argument<?> firstTypeVariable = returnTypeObject.getFirstTypeVariable().orElse(Argument.of(Object.class));
import io.micronaut.core.type.Argument;
                    .getProperty(FileWatchConfiguration.PATHS, Argument.listOf(String.class))
        return Argument.of(Session.class);

Fixed code:
    /**
     * Constant for Boolean argument.
     */
    ArgumentConversionContext<Boolean> BOOLEAN = ConversionContext.of(Argument.BOOLEAN);

    /**
     * Constant for Integer argument.
     */
    ArgumentConversionContext<Integer> INT = ConversionContext.of(Argument.INT);

    /**
     * Constant for Long argument.
     */
    ArgumentConversionContext<Long> LONG = ConversionContext.of(Argument.LONG);

    /**
     * Constant for String argument.
     */
    ArgumentConversionContext<String> STRING = ConversionContext.of(Argument.STRING);

    /**
     * Constant for List<String> argument.
     */
    ArgumentConversionContext<List<String>> LIST_OF_STRING = ConversionContext.of(Argument.LIST_OF_STRING);

import java.util.*;
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        DefaultArgumentConversionContext<?> that = (DefaultArgumentConversionContext<?>) o;
        return Objects.equals(getArgument(), that.getArgument()) &&
            Objects.equals(finalLocale, that.finalLocale) &&
            Objects.equals(finalCharset, that.finalCharset);
    }

    @Override
    public int hashCode() {
        return Objects.hash(argument, finalLocale, finalCharset);
    }

import io.micronaut.core.convert.ArgumentConversionContext;
    /**
     * Find a header and convert it to the given type.
     *
     * @param name              The name of the header
     * @param conversionContext The conversion context
     * @param <T>               The generic type
     * @return If the header is presented and can be converted an optional of the value otherwise {@link Optional#empty()}
     */
    default <T> Optional<T> getFirst(CharSequence name, ArgumentConversionContext<T> conversionContext) {
        V v = get(name);
        if (v != null) {
            return ConversionService.SHARED.convert(v, conversionContext);
        }
        return Optional.empty();
    }

        return get(name, Argument.OBJECT_ARGUMENT).isPresent();
    /**
     * Constant for string argument.
     */
    Argument<String> STRING = Argument.of(String.class);

    Argument<Integer> INT = Argument.of(int.class);
    Argument<Long> LONG = Argument.of(long.class);
    Argument<Float> FLOAT = Argument.of(float.class);
    Argument<Double> DOUBLE = Argument.of(double.class);
    Argument<Void> VOID = Argument.of(void.class);
    Argument<Byte> BYTE = Argument.of(byte.class);
    Argument<Boolean> BOOLEAN = Argument.of(boolean.class);
    Argument<Character> CHAR = Argument.of(char.class);
    Argument<Short> SHORT = Argument.of(short.class);
    /**
     * Constant for List<String> argument.
     */
    Argument<List<String>> LIST_OF_STRING = Argument.listOf(String.class);

        this(type, name, annotationMetadata, initializeTypeParameters(genericTypes), genericTypes);
    private static Map<String, Argument<?>> initializeTypeParameters(Argument[] genericTypes) {
            return Argument.STRING;
    private static final Argument ARGUMENT_PART_DATA = Argument.of(PartData.class);
                                    typeVariable = ARGUMENT_PART_DATA;
                                        typeVariable = ARGUMENT_PART_DATA;
    private static final Argument<CompletableFuture> TYPE = Argument.of(CompletableFuture.class);

        return TYPE;
    private static final Argument<Publisher> TYPE = Argument.of(Publisher.class);
        return TYPE;
import io.micronaut.core.convert.ConversionContext
        0 * headers.get(ACCESS_CONTROL_REQUEST_HEADERS, ConversionContext.of(Argument.of(List,String)))
        1 * headers.getFirst(ACCESS_CONTROL_REQUEST_METHOD, ConversionContext.of(HttpMethod.class)) >> Optional.of(HttpMethod.GET)
        1 * headers.get(ACCESS_CONTROL_REQUEST_HEADERS, ConversionContext.of(Argument.of(List,String))) >> ['foo', 'bar']
        2 * headers.getFirst(ACCESS_CONTROL_REQUEST_METHOD, ConversionContext.of(HttpMethod.class)) >> Optional.of(HttpMethod.GET)
        2 * headers.get(ACCESS_CONTROL_REQUEST_HEADERS, ConversionContext.of(Argument.of(List,String))) >> Optional.of(['foo'])
        2 * headers.get(ACCESS_CONTROL_REQUEST_HEADERS, ConversionContext.of(Argument.of(List,String))) >> Optional.of(['X-Header', 'Y-Header'])
        1 * headers.getFirst(ACCESS_CONTROL_REQUEST_METHOD, ConversionContext.of(HttpMethod.class)) >> Optional.of(HttpMethod.GET)
        2 * headers.get(ACCESS_CONTROL_REQUEST_HEADERS, ConversionContext.of(Argument.of(List,String))) >> Optional.of(['X-Header', 'Y-Header'])
        1 * headers.getFirst(ACCESS_CONTROL_REQUEST_METHOD, ConversionContext.of(HttpMethod.class)) >> Optional.of(HttpMethod.GET)
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.convert.ConversionContext;
    private static final ArgumentConversionContext<HttpMethod> CONVERSION_CONTEXT_HTTP_METHOD = ConversionContext.of(HttpMethod.class);

                    Optional<HttpMethod> result = headers.getFirst(ACCESS_CONTROL_REQUEST_METHOD, CONVERSION_CONTEXT_HTTP_METHOD);
                    Optional<List<String>> allowedHeaders = headers.get(ACCESS_CONTROL_REQUEST_HEADERS, ConversionContext.LIST_OF_STRING);
                    HttpMethod methodToMatch = preflight ? headers.getFirst(ACCESS_CONTROL_REQUEST_METHOD, CONVERSION_CONTEXT_HTTP_METHOD).orElse(requestMethod) : requestMethod;
                    Optional<List<String>> accessControlHeaders = headers.get(ACCESS_CONTROL_REQUEST_HEADERS, ConversionContext.LIST_OF_STRING);
import io.micronaut.core.convert.ArgumentConversionContext;
import java.util.List;
    private static final ArgumentConversionContext<List<HttpMethod>> CONVERSION_CONTEXT_LIST_OF_HTTP_METHOD = ConversionContext.of(Argument.listOf(HttpMethod.class));

                .get(ALLOWED_ORIGINS, ConversionContext.LIST_OF_STRING)
                .get(ALLOWED_METHODS, CONVERSION_CONTEXT_LIST_OF_HTTP_METHOD)
                .get(ALLOWED_HEADERS, ConversionContext.LIST_OF_STRING)
                .get(EXPOSED_HEADERS, ConversionContext.LIST_OF_STRING)
                .get(ALLOW_CREDENTIALS, ConversionContext.BOOLEAN)
                .get(MAX_AGE, ConversionContext.LONG)
import io.micronaut.core.convert.ConversionContext;
        return get(name, ConversionContext.INT);
        return getFirst(name, ConversionContext.STRING);
        return getFirst(HttpHeaders.CONTENT_TYPE, MediaType.CONVERSION_CONTEXT);
        Optional<Long> optional = getFirst(HttpHeaders.CONTENT_LENGTH, ConversionContext.LONG);
            .flatMap(s -> ConversionService.SHARED.convert(s, MediaType.CONVERSION_CONTEXT).map(Stream::of).orElse(Stream.empty()))
        return getFirst(CONNECTION, ConversionContext.STRING).map(val -> val.equalsIgnoreCase("keep-alive")).orElse(false);
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.convert.ConversionContext;
import io.micronaut.core.type.Argument;
    @Internal
    static final Argument<MediaType> ARGUMENT = Argument.of(MediaType.class);

    @Internal
    static final ArgumentConversionContext<MediaType> CONVERSION_CONTEXT = ConversionContext.of(ARGUMENT);

            Argument<?> firstTypeVariable = returnTypeObject.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT);
        Argument<?> firstTypeVariable = returnTypeObject.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT);
import io.micronaut.core.convert.ConversionContext;
                    .getProperty(FileWatchConfiguration.PATHS, ConversionContext.LIST_OF_STRING)
    private static final Argument<Session> TYPE = Argument.of(Session.class);

        return TYPE;