VYPR
High severityNVD Advisory· Published Aug 26, 2022· Updated Aug 3, 2024

CVE-2021-3859

CVE-2021-3859

Description

A flaw was found in Undertow that tripped the client-side invocation timeout with certain calls made over HTTP2. This flaw allows an attacker to carry out denial of service attacks.

AI Insight

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

Undertow HTTP/2 client-side invocation timeout can be tripped via crafted continuation frames, enabling denial of service.

Vulnerability

Overview

CVE-2021-3859 is a flaw in Undertow, a Java-based web server and servlet container. The vulnerability arises from incorrect handling of HTTP/2 continuation frames in the client-side HTTP/2 implementation. When certain calls are made over HTTP2, the client-side invocation timeout is improperly triggered, leading to a denial of service (DoS) condition. [1]

Exploitation

An attacker can exploit this vulnerability by sending specially crafted HTTP/2 continuation frames to an Undertow server. The issue is specifically related to how the HTTP client parses continuation frames; the commit message confirms that "continuation frames are not read correctly" [2]. This manipulation causes the server's client-side timeout logic to activate prematurely, effectively terminating the connection or stalling further processing.

Impact

Successful exploitation allows an attacker to cause a denial of service, making the Undertow-based service unavailable to legitimate users. The attack does not require authentication, as the vulnerable component is accessible during the initial HTTP/2 handshake or data exchange. [1]

Mitigation

Red Hat has acknowledged the issue and a fix was developed in commit e43f0ada3f4da6e8579e0020cec3cb1a81e487c2 and merged via pull request #1296 [2][3][4]. Users should update to a version of Undertow containing the patch. Red Hat's advisory provides guidance for affected products [1].

AI Insight generated on May 21, 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.undertow:undertow-coreMaven
< 2.2.152.2.15

Affected products

2

Patches

2
db0f5be43f8e

Merge pull request #1296 from fl4via/UNDERTOW-1979

https://github.com/undertow-io/undertowFlavia RainoneFeb 5, 2022via ghsa
3 files changed · +321 3
  • core/src/main/java/io/undertow/protocols/http2/HpackEncoder.java+1 1 modified
    @@ -163,6 +163,7 @@ public State encode(HeaderMap headers, ByteBuffer target) {
                 if (headers != currentHeaders) {
                     throw new IllegalStateException();
                 }
    +            it = headers.fiNext(it);
             }
             while (it != -1) {
                 HeaderValues values = headers.fiCurrent(it);
    @@ -239,7 +240,6 @@ public State encode(HeaderMap headers, ByteBuffer target) {
                             }
                         }
                         if(overflowing) {
    -                        it = headers.fiNext(it);
                             this.headersIterator = it;
                             this.overflowLength = current.position();
                             return State.OVERFLOW;
    
  • core/src/main/java/io/undertow/protocols/http2/Http2Channel.java+10 2 modified
    @@ -538,6 +538,16 @@ protected AbstractHttp2StreamSourceChannel createChannelImpl(FrameHeaderData fra
     
         @Override
         protected FrameHeaderData parseFrame(ByteBuffer data) throws IOException {
    +        Http2FrameHeaderParser frameParser;
    +        do {
    +            frameParser = parseFrameNoContinuation(data);
    +            // if the frame requires continuation and there is remaining data in the buffer
    +            // it should be consumed cos spec ensures the next frame is the continuation
    +        } while(frameParser != null && frameParser.getContinuationParser() != null && data.hasRemaining());
    +        return frameParser;
    +    }
    +
    +    private Http2FrameHeaderParser parseFrameNoContinuation(ByteBuffer data) throws IOException {
             if (prefaceCount < PREFACE_BYTES.length) {
                 while (data.hasRemaining() && prefaceCount < PREFACE_BYTES.length) {
                     if (data.get() != PREFACE_BYTES[prefaceCount]) {
    @@ -575,10 +585,8 @@ protected FrameHeaderData parseFrame(ByteBuffer data) throws IOException {
             }
             if (frameParser.getContinuationParser() != null) {
                 this.continuationParser = frameParser.getContinuationParser();
    -            return null;
             }
             return frameParser;
    -
         }
     
         protected void lastDataRead() {
    
  • core/src/test/java/io/undertow/client/http/H2CUpgradeContinuationTestCase.java+310 0 added
    @@ -0,0 +1,310 @@
    +/*
    + * JBoss, Home of Professional Open Source.
    + * Copyright 2021 Red Hat, Inc., and individual contributors
    + * as indicated by the @author tags.
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *     http://www.apache.org/licenses/LICENSE-2.0
    + *
    + *  Unless required by applicable law or agreed to in writing, software
    + *  distributed under the License is distributed on an "AS IS" BASIS,
    + *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + *  See the License for the specific language governing permissions and
    + *  limitations under the License.
    + */
    +package io.undertow.client.http;
    +
    +import io.undertow.Undertow;
    +import io.undertow.UndertowOptions;
    +import io.undertow.client.ClientCallback;
    +import io.undertow.client.ClientConnection;
    +import io.undertow.client.ClientExchange;
    +import io.undertow.client.ClientRequest;
    +import io.undertow.client.ClientResponse;
    +import io.undertow.client.UndertowClient;
    +import io.undertow.connector.ByteBufferPool;
    +import io.undertow.io.Receiver;
    +import io.undertow.io.Sender;
    +import io.undertow.protocols.ssl.UndertowXnioSsl;
    +import io.undertow.server.DefaultByteBufferPool;
    +import io.undertow.server.HttpHandler;
    +import io.undertow.server.HttpServerExchange;
    +import io.undertow.server.handlers.PathHandler;
    +import io.undertow.server.protocol.http2.Http2UpgradeHandler;
    +import io.undertow.testutils.DebuggingSlicePool;
    +import io.undertow.testutils.DefaultServer;
    +import io.undertow.testutils.HttpOneOnly;
    +import io.undertow.util.AttachmentKey;
    +import io.undertow.util.HeaderValues;
    +import io.undertow.util.Headers;
    +import io.undertow.util.HttpString;
    +import io.undertow.util.Methods;
    +import io.undertow.util.StatusCodes;
    +import io.undertow.util.StringReadChannelListener;
    +import io.undertow.util.StringWriteChannelListener;
    +import java.io.IOException;
    +import java.net.URI;
    +import java.util.List;
    +import java.util.concurrent.CopyOnWriteArrayList;
    +import java.util.concurrent.CountDownLatch;
    +import java.util.concurrent.TimeUnit;
    +import org.junit.AfterClass;
    +import org.junit.Assert;
    +import org.junit.BeforeClass;
    +import org.junit.Test;
    +import org.junit.runner.RunWith;
    +import org.xnio.ChannelListeners;
    +import org.xnio.IoUtils;
    +import org.xnio.OptionMap;
    +import org.xnio.Options;
    +import org.xnio.Xnio;
    +import org.xnio.XnioWorker;
    +import org.xnio.channels.StreamSinkChannel;
    +
    +/**
    + * <p>Test that uses H2C upgrade and tries to send different number of headers
    + * for a GET/POST request. The idea is that the byte buffer used in the client
    + * and server is small. That way when sending a big number of headers the
    + * HEADERS frame is not enough to contain all the data and some CONTINUATION
    + * frames are needed. The test method also tries with different sizes of DATA
    + * to force several DATA frames to be sent when using POST method.</p>
    + *
    + * @author rmartinc
    + */
    +@RunWith(DefaultServer.class)
    +@HttpOneOnly
    +public class H2CUpgradeContinuationTestCase {
    +
    +    private static final String HEADER_PREFFIX = "custom-header-";
    +    private static final String ECHO_PATH = "/echo";
    +    private static final AttachmentKey<String> RESPONSE_BODY = AttachmentKey.create(String.class);
    +
    +    private static ByteBufferPool smallPool;
    +    private static XnioWorker worker;
    +    private static Undertow server;
    +
    +    /**
    +     * Just a handler that receives the request and sends back all the custom
    +     * headers received and (if data received) returns the same data (empty
    +     * response otherwise).
    +     * @param exchange The HttpServerExchange
    +     */
    +    private static void sendEchoResponse(final HttpServerExchange exchange) {
    +        exchange.setStatusCode(StatusCodes.OK);
    +        // add the custom headers received
    +        for (HeaderValues header : exchange.getRequestHeaders()) {
    +            if (header.getFirst().startsWith(HEADER_PREFFIX)) {
    +                exchange.getResponseHeaders().putAll(header.getHeaderName(), header.subList(0, header.size()));
    +            }
    +        }
    +        // response using echo or empty string
    +        if (exchange.getRequestContentLength() > 0) {
    +            exchange.getRequestReceiver().receiveFullString(new Receiver.FullStringCallback() {
    +                @Override
    +                public void handle(HttpServerExchange exchange, String message) {
    +                    exchange.getResponseSender().send(message);
    +                }
    +            });
    +        } else {
    +            final Sender sender = exchange.getResponseSender();
    +            sender.send("");
    +        }
    +    }
    +
    +    /**
    +     * Initializes the server with the H2C handler and adds the echo handler to
    +     * manage the requests.
    +     * @throws IOException Some error
    +     */
    +    @BeforeClass
    +    public static void beforeClass() throws IOException {
    +        // server and client pool is using 1024 for the buffer size
    +        smallPool = new DebuggingSlicePool(new DefaultByteBufferPool(true, 1024, 1000, 10, 100));
    +
    +        final PathHandler path = new PathHandler()
    +                .addExactPath(ECHO_PATH, new HttpHandler() {
    +                    @Override
    +                    public void handleRequest(HttpServerExchange exchange) throws Exception {
    +                        sendEchoResponse(exchange);
    +                    }
    +                });
    +
    +        server = Undertow.builder()
    +                .setByteBufferPool(smallPool)
    +                .addHttpListener(DefaultServer.getHostPort("default") + 1, DefaultServer.getHostAddress("default"), new Http2UpgradeHandler(path))
    +                .setSocketOption(Options.REUSE_ADDRESSES, true)
    +                .build();
    +        server.start();
    +
    +        // Create xnio worker
    +        final Xnio xnio = Xnio.getInstance();
    +        final XnioWorker xnioWorker = xnio.createWorker(null, OptionMap.builder()
    +                .set(Options.WORKER_IO_THREADS, 8)
    +                .set(Options.TCP_NODELAY, true)
    +                .set(Options.KEEP_ALIVE, true)
    +                .getMap());
    +        worker = xnioWorker;
    +    }
    +
    +    /**
    +     * Stops server and worker.
    +     */
    +    @AfterClass
    +    public static void afterClass() {
    +        if (server != null) {
    +            server.stop();
    +        }
    +        if (worker != null) {
    +            worker.shutdown();
    +        }
    +        if (smallPool != null) {
    +            smallPool.close();
    +            smallPool = null;
    +        }
    +    }
    +
    +    /**
    +     * Method that sends a GET or POST request adding count number of custom
    +     * headers and sending contentLength data. GET is used if no content length
    +     * is passed, POST if contentLength is greater than 0.
    +     * @param connection The connection to use
    +     * @param requestCount The number of requests to send
    +     * @param headersCount The number of custom headers to send
    +     * @param contentLength The content length to send (POST method used if >0)
    +     * @throws Exception Some error
    +     */
    +    private void sendRequest(ClientConnection connection, int requestCount, int headersCount, int contentLength) throws Exception {
    +        final CountDownLatch latch = new CountDownLatch(requestCount);
    +        final List<ClientResponse> responses = new CopyOnWriteArrayList<>();
    +        StringBuilder sb = new StringBuilder();
    +        for (int i = 0; i < contentLength; i++) {
    +            sb.append(i % 10);
    +        }
    +        final String content = sb.length() > 0? sb.toString() : null;
    +        connection.getIoThread().execute(new Runnable() {
    +            @Override
    +            public void run() {
    +                for (int i = 0; i < requestCount; i++) {
    +                    final ClientRequest request = new ClientRequest()
    +                            .setMethod(contentLength > 0 ? Methods.POST : Methods.GET)
    +                            .setPath(ECHO_PATH);
    +                    request.getRequestHeaders().put(Headers.HOST, DefaultServer.getHostAddress());
    +                    if (contentLength > 0) {
    +                        request.getRequestHeaders().put(Headers.CONTENT_LENGTH, contentLength);
    +                    }
    +                    for (int j = 0; j < headersCount; j++) {
    +                        request.getRequestHeaders().put(new HttpString(HEADER_PREFFIX + j), HEADER_PREFFIX + j);
    +                    }
    +                    connection.sendRequest(request, createClientCallback(responses, latch, content));
    +                }
    +            }
    +        });
    +
    +        latch.await(10, TimeUnit.SECONDS);
    +
    +        Assert.assertEquals("No responses received from server in 10s", requestCount, responses.size());
    +        for (int i = 0; i < requestCount; i++) {
    +            Assert.assertEquals("Response " + i + " code was not OK", StatusCodes.OK, responses.get(i).getResponseCode());
    +            Assert.assertEquals("Incorrect data received for response " + i, contentLength > 0 ? content : "", responses.get(i).getAttachment(RESPONSE_BODY));
    +            int headersReturned = 0;
    +            for (HeaderValues header : responses.get(i).getResponseHeaders()) {
    +                if (header.getFirst().startsWith(HEADER_PREFFIX)) {
    +                    headersReturned += header.size();
    +                }
    +            }
    +            Assert.assertEquals("Incorrect number of headers returned for response " + i, headersCount, headersReturned);
    +        }
    +    }
    +
    +    /**
    +     * The real test that sends several GET and POST requests with different
    +     * number of headers and different content length.
    +     * @throws Exception  Some error
    +     */
    +    @Test
    +    public void testDifferentSizes() throws Exception {
    +        final UndertowClient client = UndertowClient.getInstance();
    +
    +        // the client connection uses the small byte-buffer of 1024 to force the continuation frames
    +        final ClientConnection connection = client.connect(
    +                new URI("http://" + DefaultServer.getHostAddress() + ":" + (DefaultServer.getHostPort("default") + 1)),
    +                worker, new UndertowXnioSsl(worker.getXnio(), OptionMap.EMPTY, DefaultServer.getClientSSLContext()),
    +                smallPool, OptionMap.create(UndertowOptions.ENABLE_HTTP2, true)).get();
    +        try {
    +            // the first request triggers the upgrade to H2C
    +            sendRequest(connection, 1, 0, 0);
    +            // send several requests with different sizes for headers and data
    +            sendRequest(connection, 10, 10, 0);
    +            sendRequest(connection, 10, 100, 0);
    +            sendRequest(connection, 10, 150, 0);
    +            sendRequest(connection, 10, 1, 10);
    +            sendRequest(connection, 10, 0, 2000);
    +            sendRequest(connection, 10, 150, 2000);
    +        } finally {
    +            IoUtils.safeClose(connection);
    +        }
    +    }
    +
    +    /**
    +     * Create the callback to receive the response and assign it to the list.
    +     * @param responses The list where the response will be added
    +     * @param latch The latch to count down when the response is received
    +     * @param message The message to send if it's a POST message (if null nothing is send)
    +     * @return The created callback
    +     */
    +    private static ClientCallback<ClientExchange> createClientCallback(final List<ClientResponse> responses, final CountDownLatch latch, String message) {
    +        return new ClientCallback<ClientExchange>() {
    +            @Override
    +            public void completed(ClientExchange result) {
    +                if (message != null) {
    +                    new StringWriteChannelListener(message).setup(result.getRequestChannel());
    +                }
    +                result.setResponseListener(new ClientCallback<ClientExchange>() {
    +                    @Override
    +                    public void completed(final ClientExchange result) {
    +                        responses.add(result.getResponse());
    +                        new StringReadChannelListener(result.getConnection().getBufferPool()) {
    +
    +                            @Override
    +                            protected void stringDone(String string) {
    +                                result.getResponse().putAttachment(RESPONSE_BODY, string);
    +                                latch.countDown();
    +                            }
    +
    +                            @Override
    +                            protected void error(IOException e) {
    +                                e.printStackTrace();
    +                                latch.countDown();
    +                            }
    +                        }.setup(result.getResponseChannel());
    +                    }
    +
    +                    @Override
    +                    public void failed(IOException e) {
    +                        e.printStackTrace();
    +                        latch.countDown();
    +                    }
    +                });
    +                try {
    +                    result.getRequestChannel().shutdownWrites();
    +                    if (!result.getRequestChannel().flush()) {
    +                        result.getRequestChannel().getWriteSetter().set(ChannelListeners.<StreamSinkChannel>flushingChannelListener(null, null));
    +                        result.getRequestChannel().resumeWrites();
    +                    }
    +                } catch (IOException e) {
    +                    e.printStackTrace();
    +                    latch.countDown();
    +                }
    +            }
    +
    +            @Override
    +            public void failed(IOException e) {
    +                e.printStackTrace();
    +                latch.countDown();
    +            }
    +        };
    +    }
    +}
    
e43f0ada3f4d

[UNDERTOW-1979] CVE-2021-3859 continuation frames are not read correctly

https://github.com/undertow-io/undertowrmartincSep 16, 2021via ghsa
3 files changed · +321 3
  • core/src/main/java/io/undertow/protocols/http2/HpackEncoder.java+1 1 modified
    @@ -163,6 +163,7 @@ public State encode(HeaderMap headers, ByteBuffer target) {
                 if (headers != currentHeaders) {
                     throw new IllegalStateException();
                 }
    +            it = headers.fiNext(it);
             }
             while (it != -1) {
                 HeaderValues values = headers.fiCurrent(it);
    @@ -239,7 +240,6 @@ public State encode(HeaderMap headers, ByteBuffer target) {
                             }
                         }
                         if(overflowing) {
    -                        it = headers.fiNext(it);
                             this.headersIterator = it;
                             this.overflowLength = current.position();
                             return State.OVERFLOW;
    
  • core/src/main/java/io/undertow/protocols/http2/Http2Channel.java+10 2 modified
    @@ -538,6 +538,16 @@ protected AbstractHttp2StreamSourceChannel createChannelImpl(FrameHeaderData fra
     
         @Override
         protected FrameHeaderData parseFrame(ByteBuffer data) throws IOException {
    +        Http2FrameHeaderParser frameParser;
    +        do {
    +            frameParser = parseFrameNoContinuation(data);
    +            // if the frame requires continuation and there is remaining data in the buffer
    +            // it should be consumed cos spec ensures the next frame is the continuation
    +        } while(frameParser != null && frameParser.getContinuationParser() != null && data.hasRemaining());
    +        return frameParser;
    +    }
    +
    +    private Http2FrameHeaderParser parseFrameNoContinuation(ByteBuffer data) throws IOException {
             if (prefaceCount < PREFACE_BYTES.length) {
                 while (data.hasRemaining() && prefaceCount < PREFACE_BYTES.length) {
                     if (data.get() != PREFACE_BYTES[prefaceCount]) {
    @@ -575,10 +585,8 @@ protected FrameHeaderData parseFrame(ByteBuffer data) throws IOException {
             }
             if (frameParser.getContinuationParser() != null) {
                 this.continuationParser = frameParser.getContinuationParser();
    -            return null;
             }
             return frameParser;
    -
         }
     
         protected void lastDataRead() {
    
  • core/src/test/java/io/undertow/client/http/H2CUpgradeContinuationTestCase.java+310 0 added
    @@ -0,0 +1,310 @@
    +/*
    + * JBoss, Home of Professional Open Source.
    + * Copyright 2021 Red Hat, Inc., and individual contributors
    + * as indicated by the @author tags.
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *     http://www.apache.org/licenses/LICENSE-2.0
    + *
    + *  Unless required by applicable law or agreed to in writing, software
    + *  distributed under the License is distributed on an "AS IS" BASIS,
    + *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + *  See the License for the specific language governing permissions and
    + *  limitations under the License.
    + */
    +package io.undertow.client.http;
    +
    +import io.undertow.Undertow;
    +import io.undertow.UndertowOptions;
    +import io.undertow.client.ClientCallback;
    +import io.undertow.client.ClientConnection;
    +import io.undertow.client.ClientExchange;
    +import io.undertow.client.ClientRequest;
    +import io.undertow.client.ClientResponse;
    +import io.undertow.client.UndertowClient;
    +import io.undertow.connector.ByteBufferPool;
    +import io.undertow.io.Receiver;
    +import io.undertow.io.Sender;
    +import io.undertow.protocols.ssl.UndertowXnioSsl;
    +import io.undertow.server.DefaultByteBufferPool;
    +import io.undertow.server.HttpHandler;
    +import io.undertow.server.HttpServerExchange;
    +import io.undertow.server.handlers.PathHandler;
    +import io.undertow.server.protocol.http2.Http2UpgradeHandler;
    +import io.undertow.testutils.DebuggingSlicePool;
    +import io.undertow.testutils.DefaultServer;
    +import io.undertow.testutils.HttpOneOnly;
    +import io.undertow.util.AttachmentKey;
    +import io.undertow.util.HeaderValues;
    +import io.undertow.util.Headers;
    +import io.undertow.util.HttpString;
    +import io.undertow.util.Methods;
    +import io.undertow.util.StatusCodes;
    +import io.undertow.util.StringReadChannelListener;
    +import io.undertow.util.StringWriteChannelListener;
    +import java.io.IOException;
    +import java.net.URI;
    +import java.util.List;
    +import java.util.concurrent.CopyOnWriteArrayList;
    +import java.util.concurrent.CountDownLatch;
    +import java.util.concurrent.TimeUnit;
    +import org.junit.AfterClass;
    +import org.junit.Assert;
    +import org.junit.BeforeClass;
    +import org.junit.Test;
    +import org.junit.runner.RunWith;
    +import org.xnio.ChannelListeners;
    +import org.xnio.IoUtils;
    +import org.xnio.OptionMap;
    +import org.xnio.Options;
    +import org.xnio.Xnio;
    +import org.xnio.XnioWorker;
    +import org.xnio.channels.StreamSinkChannel;
    +
    +/**
    + * <p>Test that uses H2C upgrade and tries to send different number of headers
    + * for a GET/POST request. The idea is that the byte buffer used in the client
    + * and server is small. That way when sending a big number of headers the
    + * HEADERS frame is not enough to contain all the data and some CONTINUATION
    + * frames are needed. The test method also tries with different sizes of DATA
    + * to force several DATA frames to be sent when using POST method.</p>
    + *
    + * @author rmartinc
    + */
    +@RunWith(DefaultServer.class)
    +@HttpOneOnly
    +public class H2CUpgradeContinuationTestCase {
    +
    +    private static final String HEADER_PREFFIX = "custom-header-";
    +    private static final String ECHO_PATH = "/echo";
    +    private static final AttachmentKey<String> RESPONSE_BODY = AttachmentKey.create(String.class);
    +
    +    private static ByteBufferPool smallPool;
    +    private static XnioWorker worker;
    +    private static Undertow server;
    +
    +    /**
    +     * Just a handler that receives the request and sends back all the custom
    +     * headers received and (if data received) returns the same data (empty
    +     * response otherwise).
    +     * @param exchange The HttpServerExchange
    +     */
    +    private static void sendEchoResponse(final HttpServerExchange exchange) {
    +        exchange.setStatusCode(StatusCodes.OK);
    +        // add the custom headers received
    +        for (HeaderValues header : exchange.getRequestHeaders()) {
    +            if (header.getFirst().startsWith(HEADER_PREFFIX)) {
    +                exchange.getResponseHeaders().putAll(header.getHeaderName(), header.subList(0, header.size()));
    +            }
    +        }
    +        // response using echo or empty string
    +        if (exchange.getRequestContentLength() > 0) {
    +            exchange.getRequestReceiver().receiveFullString(new Receiver.FullStringCallback() {
    +                @Override
    +                public void handle(HttpServerExchange exchange, String message) {
    +                    exchange.getResponseSender().send(message);
    +                }
    +            });
    +        } else {
    +            final Sender sender = exchange.getResponseSender();
    +            sender.send("");
    +        }
    +    }
    +
    +    /**
    +     * Initializes the server with the H2C handler and adds the echo handler to
    +     * manage the requests.
    +     * @throws IOException Some error
    +     */
    +    @BeforeClass
    +    public static void beforeClass() throws IOException {
    +        // server and client pool is using 1024 for the buffer size
    +        smallPool = new DebuggingSlicePool(new DefaultByteBufferPool(true, 1024, 1000, 10, 100));
    +
    +        final PathHandler path = new PathHandler()
    +                .addExactPath(ECHO_PATH, new HttpHandler() {
    +                    @Override
    +                    public void handleRequest(HttpServerExchange exchange) throws Exception {
    +                        sendEchoResponse(exchange);
    +                    }
    +                });
    +
    +        server = Undertow.builder()
    +                .setByteBufferPool(smallPool)
    +                .addHttpListener(DefaultServer.getHostPort("default") + 1, DefaultServer.getHostAddress("default"), new Http2UpgradeHandler(path))
    +                .setSocketOption(Options.REUSE_ADDRESSES, true)
    +                .build();
    +        server.start();
    +
    +        // Create xnio worker
    +        final Xnio xnio = Xnio.getInstance();
    +        final XnioWorker xnioWorker = xnio.createWorker(null, OptionMap.builder()
    +                .set(Options.WORKER_IO_THREADS, 8)
    +                .set(Options.TCP_NODELAY, true)
    +                .set(Options.KEEP_ALIVE, true)
    +                .getMap());
    +        worker = xnioWorker;
    +    }
    +
    +    /**
    +     * Stops server and worker.
    +     */
    +    @AfterClass
    +    public static void afterClass() {
    +        if (server != null) {
    +            server.stop();
    +        }
    +        if (worker != null) {
    +            worker.shutdown();
    +        }
    +        if (smallPool != null) {
    +            smallPool.close();
    +            smallPool = null;
    +        }
    +    }
    +
    +    /**
    +     * Method that sends a GET or POST request adding count number of custom
    +     * headers and sending contentLength data. GET is used if no content length
    +     * is passed, POST if contentLength is greater than 0.
    +     * @param connection The connection to use
    +     * @param requestCount The number of requests to send
    +     * @param headersCount The number of custom headers to send
    +     * @param contentLength The content length to send (POST method used if >0)
    +     * @throws Exception Some error
    +     */
    +    private void sendRequest(ClientConnection connection, int requestCount, int headersCount, int contentLength) throws Exception {
    +        final CountDownLatch latch = new CountDownLatch(requestCount);
    +        final List<ClientResponse> responses = new CopyOnWriteArrayList<>();
    +        StringBuilder sb = new StringBuilder();
    +        for (int i = 0; i < contentLength; i++) {
    +            sb.append(i % 10);
    +        }
    +        final String content = sb.length() > 0? sb.toString() : null;
    +        connection.getIoThread().execute(new Runnable() {
    +            @Override
    +            public void run() {
    +                for (int i = 0; i < requestCount; i++) {
    +                    final ClientRequest request = new ClientRequest()
    +                            .setMethod(contentLength > 0 ? Methods.POST : Methods.GET)
    +                            .setPath(ECHO_PATH);
    +                    request.getRequestHeaders().put(Headers.HOST, DefaultServer.getHostAddress());
    +                    if (contentLength > 0) {
    +                        request.getRequestHeaders().put(Headers.CONTENT_LENGTH, contentLength);
    +                    }
    +                    for (int j = 0; j < headersCount; j++) {
    +                        request.getRequestHeaders().put(new HttpString(HEADER_PREFFIX + j), HEADER_PREFFIX + j);
    +                    }
    +                    connection.sendRequest(request, createClientCallback(responses, latch, content));
    +                }
    +            }
    +        });
    +
    +        latch.await(10, TimeUnit.SECONDS);
    +
    +        Assert.assertEquals("No responses received from server in 10s", requestCount, responses.size());
    +        for (int i = 0; i < requestCount; i++) {
    +            Assert.assertEquals("Response " + i + " code was not OK", StatusCodes.OK, responses.get(i).getResponseCode());
    +            Assert.assertEquals("Incorrect data received for response " + i, contentLength > 0 ? content : "", responses.get(i).getAttachment(RESPONSE_BODY));
    +            int headersReturned = 0;
    +            for (HeaderValues header : responses.get(i).getResponseHeaders()) {
    +                if (header.getFirst().startsWith(HEADER_PREFFIX)) {
    +                    headersReturned += header.size();
    +                }
    +            }
    +            Assert.assertEquals("Incorrect number of headers returned for response " + i, headersCount, headersReturned);
    +        }
    +    }
    +
    +    /**
    +     * The real test that sends several GET and POST requests with different
    +     * number of headers and different content length.
    +     * @throws Exception  Some error
    +     */
    +    @Test
    +    public void testDifferentSizes() throws Exception {
    +        final UndertowClient client = UndertowClient.getInstance();
    +
    +        // the client connection uses the small byte-buffer of 1024 to force the continuation frames
    +        final ClientConnection connection = client.connect(
    +                new URI("http://" + DefaultServer.getHostAddress() + ":" + (DefaultServer.getHostPort("default") + 1)),
    +                worker, new UndertowXnioSsl(worker.getXnio(), OptionMap.EMPTY, DefaultServer.getClientSSLContext()),
    +                smallPool, OptionMap.create(UndertowOptions.ENABLE_HTTP2, true)).get();
    +        try {
    +            // the first request triggers the upgrade to H2C
    +            sendRequest(connection, 1, 0, 0);
    +            // send several requests with different sizes for headers and data
    +            sendRequest(connection, 10, 10, 0);
    +            sendRequest(connection, 10, 100, 0);
    +            sendRequest(connection, 10, 150, 0);
    +            sendRequest(connection, 10, 1, 10);
    +            sendRequest(connection, 10, 0, 2000);
    +            sendRequest(connection, 10, 150, 2000);
    +        } finally {
    +            IoUtils.safeClose(connection);
    +        }
    +    }
    +
    +    /**
    +     * Create the callback to receive the response and assign it to the list.
    +     * @param responses The list where the response will be added
    +     * @param latch The latch to count down when the response is received
    +     * @param message The message to send if it's a POST message (if null nothing is send)
    +     * @return The created callback
    +     */
    +    private static ClientCallback<ClientExchange> createClientCallback(final List<ClientResponse> responses, final CountDownLatch latch, String message) {
    +        return new ClientCallback<ClientExchange>() {
    +            @Override
    +            public void completed(ClientExchange result) {
    +                if (message != null) {
    +                    new StringWriteChannelListener(message).setup(result.getRequestChannel());
    +                }
    +                result.setResponseListener(new ClientCallback<ClientExchange>() {
    +                    @Override
    +                    public void completed(final ClientExchange result) {
    +                        responses.add(result.getResponse());
    +                        new StringReadChannelListener(result.getConnection().getBufferPool()) {
    +
    +                            @Override
    +                            protected void stringDone(String string) {
    +                                result.getResponse().putAttachment(RESPONSE_BODY, string);
    +                                latch.countDown();
    +                            }
    +
    +                            @Override
    +                            protected void error(IOException e) {
    +                                e.printStackTrace();
    +                                latch.countDown();
    +                            }
    +                        }.setup(result.getResponseChannel());
    +                    }
    +
    +                    @Override
    +                    public void failed(IOException e) {
    +                        e.printStackTrace();
    +                        latch.countDown();
    +                    }
    +                });
    +                try {
    +                    result.getRequestChannel().shutdownWrites();
    +                    if (!result.getRequestChannel().flush()) {
    +                        result.getRequestChannel().getWriteSetter().set(ChannelListeners.<StreamSinkChannel>flushingChannelListener(null, null));
    +                        result.getRequestChannel().resumeWrites();
    +                    }
    +                } catch (IOException e) {
    +                    e.printStackTrace();
    +                    latch.countDown();
    +                }
    +            }
    +
    +            @Override
    +            public void failed(IOException e) {
    +                e.printStackTrace();
    +                latch.countDown();
    +            }
    +        };
    +    }
    +}
    

Vulnerability mechanics

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

References

11

News mentions

0

No linked articles in our index yet.