VYPR
High severityNVD Advisory· Published Mar 20, 2026· Updated Mar 20, 2026

Micronaut Framework vulnerable to a Denial of Service in HTML error response caching

CVE-2026-33012

Description

Micronaut Framework is a JVM-based full stack Java framework designed for building modular, easily testable JVM applications. Versions 4.7.0 through 4.10.16 used an unbounded ConcurrentHashMap cache with no eviction policy in its DefaultHtmlErrorResponseBodyProvider. If the application throws an exception whose message may be influenced by an attacker, (for example, including request query value parameters) it could be used by remote attackers to cause an unbounded heap growth and OutOfMemoryError, leading to DoS. This issue has been fixed in version 4.10.7.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Micronaut Framework 4.7.0–4.10.16 uses an unbounded cache in DefaultHtmlErrorResponseBodyProvider, allowing remote DoS via attacker-controlled exception messages.

Vulnerability

CVE-2026-33012 affects Micronaut Framework versions 4.7.0 through 4.10.16. The DefaultHtmlErrorResponseBodyProvider uses an unbounded ConcurrentHashMap cache with no eviction policy. policy. When an application throws an exception whose message can be influenced by an attacker (e.g., via request query parameters), each unique message is cached indefinitely, leading to unbounded heap growth and an OutOfMemoryError [1].

Exploitation

An attacker can send crafted requests that cause the application to generate exception messages containing attacker-controlled content. Because the cache key is derived from the exception message, each distinct message creates a new cache entry. No authentication is required; the attack is performed over the network by sending HTTP requests to the vulnerable endpoint [1].

Impact

Successful exploitation results in a denial of service (DoS) due to memory exhaustion. The application may crash or become unresponsive, affecting availability. The vulnerability does not require any special privileges or user interaction beyond sending malicious requests [1].

Mitigation

The issue is fixed in Micronaut Core version 4.10.7. The fix replaces the unbounded ConcurrentHashMap with a synchronizedMap that enforces a maximum cache size, preventing unbounded growth [2]. Users should upgrade to 4.10.7 or later. No workarounds are documented.

AI Insight generated on May 18, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
io.micronaut:micronaut-http-serverMaven
>= 4.7.0, < 4.10.174.10.17

Affected products

2

Patches

1
1e2ba2c14386

Replace ConcurrentHashMap with synchronizedMap

2 files changed · +96 2
  • http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProvider.java+10 2 modified
    @@ -21,6 +21,7 @@
     import io.micronaut.core.annotation.NonNull;
     import io.micronaut.core.util.LocaleResolver;
     import io.micronaut.core.util.StringUtils;
    +import io.micronaut.core.util.clhm.ConcurrentLinkedHashMap;
     import io.micronaut.http.HttpRequest;
     import io.micronaut.http.HttpResponse;
     import io.micronaut.http.HttpStatus;
    @@ -32,7 +33,6 @@
     import java.util.List;
     import java.util.Locale;
     import java.util.Map;
    -import java.util.concurrent.ConcurrentHashMap;
     
     import static io.micronaut.http.HttpStatus.*;
     
    @@ -128,10 +128,14 @@ final class DefaultHtmlErrorResponseBodyProvider implements HtmlErrorResponseBod
                                       }
                 """;
     
    +    private static final int MAX_CACHE_SIZE = 100;
    +
         private final HtmlSanitizer htmlSanitizer;
         private final MessageSource messageSource;
         private final LocaleResolver<HttpRequest<?>> localeResolver;
    -    private final Map<HtmlErrorPage, String> cache = new ConcurrentHashMap<>();
    +    private final Map<HtmlErrorPage, String> cache = new ConcurrentLinkedHashMap.Builder<HtmlErrorPage, String>()
    +        .maximumWeightedCapacity(MAX_CACHE_SIZE)
    +        .build();
     
         DefaultHtmlErrorResponseBodyProvider(HtmlSanitizer htmlSanitizer,
                                              MessageSource messageSource,
    @@ -217,4 +221,8 @@ private record HtmlErrorPage(Locale locale,
                                      String errorBold,
                                      List<String> messages) {
         }
    +
    +    Map<HtmlErrorPage, String> getCache() {
    +        return cache;
    +    }
     }
    
  • test-suite/src/test/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProviderCacheMaxSizeTest.java+86 0 added
    @@ -0,0 +1,86 @@
    +package io.micronaut.http.server.exceptions.response;
    +
    +import io.micronaut.context.annotation.Property;
    +import io.micronaut.context.annotation.Requires;
    +import io.micronaut.http.HttpRequest;
    +import io.micronaut.http.HttpStatus;
    +import io.micronaut.http.MediaType;
    +import io.micronaut.http.annotation.Controller;
    +import io.micronaut.http.annotation.Post;
    +import io.micronaut.http.annotation.Produces;
    +import io.micronaut.http.annotation.Status;
    +import io.micronaut.http.client.BlockingHttpClient;
    +import io.micronaut.http.client.HttpClient;
    +import io.micronaut.http.client.annotation.Client;
    +import io.micronaut.http.client.exceptions.HttpClientResponseException;
    +import io.micronaut.http.uri.UriBuilder;
    +import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
    +import org.junit.jupiter.api.Test;
    +
    +import java.net.URI;
    +import java.util.Collections;
    +
    +import static org.junit.jupiter.api.Assertions.assertEquals;
    +import static org.junit.jupiter.api.Assertions.assertThrows;
    +import static org.junit.jupiter.api.Assertions.assertTrue;
    +
    +@Property(name = "spec.name", value = "DefaultHtmlErrorResponseBodyProviderCacheMaxSize")
    +@MicronautTest
    +class DefaultHtmlErrorResponseBodyProviderCacheMaxSizeTest {
    +
    +    @Test
    +    void errorResponseCacheHasAMaxSize(@Client("/") HttpClient httpClient,
    +                                       DefaultHtmlErrorResponseBodyProvider defaultHtmlErrorResponseBodyProvider) {
    +        BlockingHttpClient client = httpClient.toBlocking();
    +        int max = 10;
    +        for (int i = 1; i <= max; i++) {
    +            URI uri = UriBuilder.of("/errorcache").queryParam("nonce", String.valueOf(i)).build();
    +            HttpRequest<?> request = HttpRequest.POST(uri, Collections.emptyMap())
    +                .accept(MediaType.TEXT_HTML);
    +            HttpClientResponseException ex = assertThrows(HttpClientResponseException.class, () -> client.exchange(request));
    +            assertEquals(HttpStatus.NOT_ACCEPTABLE, ex.getStatus());
    +        }
    +        assertEquals(1, defaultHtmlErrorResponseBodyProvider.getCache().size());
    +    }
    +
    +    @Test
    +    void errorResponseCacheHasAMaxSizeForCustomException(@Client("/") HttpClient httpClient,
    +                                       DefaultHtmlErrorResponseBodyProvider defaultHtmlErrorResponseBodyProvider) {
    +        BlockingHttpClient client = httpClient.toBlocking();
    +        int size = 110;
    +        int max = 100;
    +        assertTrue(max < size);
    +        for (int i = 1; i <= size; i++) {
    +            URI uri = UriBuilder.of("/errorcache").path("/ex").queryParam("nonce", String.valueOf(i)).build();
    +            HttpRequest<?> request = HttpRequest.POST(uri, Collections.emptyMap())
    +                .accept(MediaType.TEXT_HTML);
    +            HttpClientResponseException ex = assertThrows(HttpClientResponseException.class, () -> client.exchange(request));
    +            assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, ex.getStatus());
    +        }
    +        assertEquals(max, defaultHtmlErrorResponseBodyProvider.getCache().size());
    +    }
    +
    +    @Requires(property = "spec.name", value = "DefaultHtmlErrorResponseBodyProviderCacheMaxSize")
    +    @Controller("/errorcache")
    +    static class OnlyPostController {
    +        @Produces(MediaType.TEXT_PLAIN)
    +        @Status(HttpStatus.OK)
    +        @Post
    +        void index() {
    +        }
    +
    +        @Produces(MediaType.TEXT_HTML)
    +        @Post("/ex")
    +        @Status(HttpStatus.OK)
    +        void ex(HttpRequest<?> request) {
    +            throw  new CustomException(request);
    +        }
    +    }
    +
    +    static class CustomException extends RuntimeException {
    +        CustomException(HttpRequest<?> request) {
    +            super("foobar "+ request.getUri());
    +        }
    +    }
    +
    +}
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.