VYPR
High severityOSV Advisory· Published Feb 19, 2024· Updated Mar 17, 2026

Undertow: out-of-memory error after several closed connections with wildfly-http-client protocol

CVE-2024-1635

Description

A vulnerability was found in Undertow. This vulnerability impacts a server that supports the wildfly-http-client protocol. Whenever a malicious user opens and closes a connection with the HTTP port of the server and then closes the connection immediately, the server will end with both memory and open file limits exhausted at some point, depending on the amount of memory available.

At HTTP upgrade to remoting, the WriteTimeoutStreamSinkConduit leaks connections if RemotingConnection is closed by Remoting ServerConnectionOpenListener. Because the remoting connection originates in Undertow as part of the HTTP upgrade, there is an external layer to the remoting connection. This connection is unaware of the outermost layer when closing the connection during the connection opening procedure. Hence, the Undertow WriteTimeoutStreamSinkConduit is not notified of the closed connection in this scenario. Because WriteTimeoutStreamSinkConduit creates a timeout task, the whole dependency tree leaks via that task, which is added to XNIO WorkerThread. So, the workerThread points to the Undertow conduit, which contains the connections and causes the leak.

AI Insight

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

Undertow vulnerability allows remote attackers to exhaust server resources by repeatedly opening and closing HTTP connections, leading to denial of service.

Vulnerability

Details

CVE-2024-1635 is a resource exhaustion vulnerability in Undertow, a web server used in Red Hat JBoss Enterprise Application Platform (EAP). The root cause is a connection leak in the WriteTimeoutStreamSinkConduit during the HTTP upgrade to the remoting protocol. When a RemotingConnection is closed by the RemotingServerConnectionOpenListener, the Undertow conduit is not notified, leaving a timeout task that holds references to the connection. This causes a cascading leak of memory and open file handles [1][2].

Attack

Vector

An attacker can exploit this vulnerability without authentication by repeatedly opening and closing HTTP connections to the server's HTTP port. This triggers the connection leak each time, gradually exhausting server resources. The attack requires network access to the server and the server must support the wildfly-http-client protocol [2].

Impact

Successful exploitation leads to memory exhaustion and depletion of file descriptors, resulting in a denial of service (DoS) condition. The server may become unresponsive or crash, depending on available resources [1].

Mitigation

Red Hat has released patches for affected versions of JBoss EAP 7.x via RHSA-2024:1674 (for RHEL 7) and RHSA-2024:1675 (for RHEL 8) [3][4]. Users should apply these updates to mitigate the vulnerability. No workarounds are currently known.

AI Insight generated on May 20, 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.3.0.Final, < 2.3.12.Final2.3.12.Final
io.undertow:undertow-coreMaven
< 2.2.31.Final2.2.31.Final

Affected products

2

Patches

2
3cdb104e225f

Merge pull request #1559 from fl4via/2.2.x_backport_bug_fixes

https://github.com/undertow-io/undertowFlavia RainoneMar 2, 2024via ghsa
5 files changed · +126 23
  • core/src/main/java/io/undertow/conduits/WriteTimeoutStreamSinkConduit.java+61 21 modified
    @@ -22,8 +22,8 @@
     import io.undertow.UndertowOptions;
     import io.undertow.server.OpenListener;
     import io.undertow.util.WorkerUtils;
    -
     import org.xnio.Buffers;
    +import org.xnio.ChannelListener;
     import org.xnio.ChannelListeners;
     import org.xnio.IoUtils;
     import org.xnio.Options;
    @@ -47,7 +47,7 @@
      */
     public final class WriteTimeoutStreamSinkConduit extends AbstractStreamSinkConduit<StreamSinkConduit> {
     
    -    private XnioExecutor.Key handle;
    +    private volatile XnioExecutor.Key handle;
         private final StreamConnection connection;
         private volatile long expireTime = -1;
         private final OpenListener openListener;
    @@ -82,6 +82,16 @@ public WriteTimeoutStreamSinkConduit(final StreamSinkConduit delegate, StreamCon
             super(delegate);
             this.connection = connection;
             this.openListener = openListener;
    +        this.connection.getCloseSetter().set((ChannelListener<StreamConnection>) channel -> {
    +            if (handle != null) {
    +                synchronized (WriteTimeoutStreamSinkConduit.this) {
    +                    if (handle != null) {
    +                        handle.remove();
    +                        handle = null;
    +                    }
    +                }
    +            }
    +        });
         }
     
         private void handleWriteTimeout(final long ret) throws IOException {
    @@ -124,10 +134,14 @@ public long write(final ByteBuffer[] srcs, final int offset, final int length) t
         public int writeFinal(ByteBuffer src) throws IOException {
             int ret = super.writeFinal(src);
             handleWriteTimeout(ret);
    -        if(!src.hasRemaining()) {
    -            if(handle != null) {
    -                handle.remove();
    -                handle = null;
    +        if (!src.hasRemaining()) {
    +            if (handle != null) {
    +                synchronized (this) {
    +                    if (handle != null) {
    +                        handle.remove();
    +                        handle = null;
    +                    }
    +                }
                 }
             }
             return ret;
    @@ -137,10 +151,14 @@ public int writeFinal(ByteBuffer src) throws IOException {
         public long writeFinal(ByteBuffer[] srcs, int offset, int length) throws IOException {
             long ret = super.writeFinal(srcs, offset, length);
             handleWriteTimeout(ret);
    -        if(!Buffers.hasRemaining(srcs)) {
    -            if(handle != null) {
    -                handle.remove();
    -                handle = null;
    +        if (!Buffers.hasRemaining(srcs)) {
    +            if (handle != null) {
    +                synchronized (this) {
    +                    if (handle != null) {
    +                        handle.remove();
    +                        handle = null;
    +                    }
    +                }
                 }
             }
             return ret;
    @@ -200,19 +218,33 @@ private Integer getTimeout() {
     
         @Override
         public void terminateWrites() throws IOException {
    -        super.terminateWrites();
    -        if(handle != null) {
    -            handle.remove();
    -            handle = null;
    +        try {
    +            super.terminateWrites();
    +        } finally {
    +            if(handle != null) {
    +                synchronized (this) {
    +                    if (this.handle != null) {
    +                        handle.remove();
    +                        handle = null;
    +                    }
    +                }
    +            }
             }
         }
     
         @Override
         public void truncateWrites() throws IOException {
    -        super.truncateWrites();
    -        if(handle != null) {
    -            handle.remove();
    -            handle = null;
    +        try {
    +            super.truncateWrites();
    +        } finally {
    +            if (handle != null) {
    +                synchronized (this) {
    +                    if (this.handle != null) {
    +                        handle.remove();
    +                        handle = null;
    +                    }
    +                }
    +            }
             }
         }
     
    @@ -233,8 +265,12 @@ public void suspendWrites() {
     
             XnioExecutor.Key handle = this.handle;
             if(handle != null) {
    -            handle.remove();
    -            this.handle = null;
    +            synchronized (this) {
    +                if (this.handle != null) {
    +                    handle.remove();
    +                    this.handle = null;
    +                }
    +            }
             }
         }
     
    @@ -253,7 +289,11 @@ private void handleResumeTimeout() {
             expireTime = currentTime + timeout;
             XnioExecutor.Key key = handle;
             if (key == null) {
    -            handle = connection.getIoThread().executeAfter(timeoutCommand, timeout, TimeUnit.MILLISECONDS);
    +            synchronized (this) {
    +                if (handle == null) {
    +                    handle = connection.getIoThread().executeAfter(timeoutCommand, timeout, TimeUnit.MILLISECONDS);
    +                }
    +            }
             }
         }
     }
    
  • core/src/main/java/io/undertow/server/protocol/ajp/AjpReadListener.java+2 2 modified
    @@ -19,6 +19,7 @@
     package io.undertow.server.protocol.ajp;
     
     import io.undertow.UndertowLogger;
    +import io.undertow.UndertowMessages;
     import io.undertow.UndertowOptions;
     import io.undertow.conduits.ConduitListener;
     import io.undertow.conduits.EmptyStreamSourceConduit;
    @@ -165,8 +166,7 @@ public void handleEvent(final StreamSourceChannel channel) {
                     }
                     if (read > maxRequestSize) {
                         UndertowLogger.REQUEST_LOGGER.requestHeaderWasTooLarge(connection.getPeerAddress(), maxRequestSize);
    -                    safeClose(connection);
    -                    return;
    +                    throw UndertowMessages.MESSAGES.badRequest();
                     }
                 } while (!state.isComplete());
     
    
  • core/src/main/java/io/undertow/server/protocol/http/HttpRequestParser.java+23 0 modified
    @@ -372,6 +372,10 @@ private void handleStateful(ByteBuffer buffer, ParseState currentState, HttpServ
         private static final int IN_PATH = 4;
         private static final int HOST_DONE = 5;
     
    +    private static final int PATH_SEGMENT_START = 0;
    +    private static final int PATH_DOT_SEGMENT = 1;
    +    private static final int PATH_NON_DOT_SEGMENT = 2;
    +
         /**
          * Parses a path value
          *
    @@ -387,6 +391,8 @@ final void handlePath(ByteBuffer buffer, ParseState state, HttpServerExchange ex
             int canonicalPathStart = state.pos;
             boolean urlDecodeRequired = state.urlDecodeRequired;
     
    +        int pathSubState = 0;
    +
             while (buffer.hasRemaining()) {
                 char next = (char) (buffer.get() & 0xFF);
                 if(!allowUnescapedCharactersInUrl && !ALLOWED_TARGET_CHARACTER[next]) {
    @@ -410,6 +416,11 @@ final void handlePath(ByteBuffer buffer, ParseState state, HttpServerExchange ex
                     state.urlDecodeRequired = urlDecodeRequired;
                     // store at canonical path the partial path parsed up until here
                     state.canonicalPath.append(stringBuilder.substring(canonicalPathStart));
    +                if (parseState == IN_PATH && pathSubState == PATH_DOT_SEGMENT) {
    +                    // Inside a dot-segment (".", ".."), we don't want to allow removal of the ';' character from
    +                    // the path. This is to avoid path traversal issues - "/..;" should not be treated as "/..".
    +                    state.canonicalPath.append(";");
    +                }
                     state.stringBuilder.append(";");
                     // set position to end of path (possibly start of parameter name)
                     state.pos = state.stringBuilder.length();
    @@ -443,6 +454,18 @@ final void handlePath(ByteBuffer buffer, ParseState state, HttpServerExchange ex
                     } else if (next == '/' && parseState != HOST_DONE) {
                         parseState = IN_PATH;
                     }
    +
    +                // This is helper state that tracks if the parser is currently in a path dot-segment (".", "..") or not.
    +                if (parseState == IN_PATH) {
    +                    if (next == '/') {
    +                        pathSubState = PATH_SEGMENT_START;
    +                    } else if (next == '.' && (pathSubState == PATH_SEGMENT_START || pathSubState == PATH_DOT_SEGMENT)) {
    +                        pathSubState = PATH_DOT_SEGMENT;
    +                    } else {
    +                        pathSubState = PATH_NON_DOT_SEGMENT;
    +                    }
    +                }
    +
                     stringBuilder.append(next);
                 }
     
    
  • core/src/main/java/io/undertow/server/protocol/http/ParseState.java+1 0 modified
    @@ -142,6 +142,7 @@ public void reset() {
             this.leftOver = 0;
             this.urlDecodeRequired = false;
             this.stringBuilder.setLength(0);
    +        this.canonicalPath.setLength(0);
             this.nextHeader = null;
             this.nextQueryParam = null;
             this.mapCount = 0;
    
  • core/src/test/java/io/undertow/server/protocol/http/SimpleParserTestCase.java+39 0 modified
    @@ -676,6 +676,45 @@ public void testNonEncodedAsciiCharactersExplicitlyAllowed() throws UnsupportedE
             Assert.assertEquals("/bår", result.getRequestURI()); //not decoded
         }
     
    +    @Test
    +    public void testDirectoryTraversal() throws Exception {
    +        byte[] in = "GET /path/..;/ HTTP/1.1\r\n\r\n".getBytes();
    +        ParseState context = new ParseState(10);
    +        HttpServerExchange result = new HttpServerExchange(null);
    +        HttpRequestParser.instance(OptionMap.EMPTY).handle(ByteBuffer.wrap(in), context, result);
    +        Assert.assertEquals("/path/..;/", result.getRequestURI());
    +        Assert.assertEquals("/path/..;/", result.getRequestPath());
    +        Assert.assertEquals("/path/..;/", result.getRelativePath());
    +        Assert.assertEquals("", result.getQueryString());
    +
    +        in = "GET /path/../ HTTP/1.1\r\n\r\n".getBytes();
    +        context = new ParseState(10);
    +        result = new HttpServerExchange(null);
    +        HttpRequestParser.instance(OptionMap.EMPTY).handle(ByteBuffer.wrap(in), context, result);
    +        Assert.assertEquals("/path/../", result.getRequestURI());
    +        Assert.assertEquals("/path/../", result.getRequestPath());
    +        Assert.assertEquals("/path/../", result.getRelativePath());
    +        Assert.assertEquals("", result.getQueryString());
    +
    +        in = "GET /path/..?/ HTTP/1.1\r\n\r\n".getBytes();
    +        context = new ParseState(10);
    +        result = new HttpServerExchange(null);
    +        HttpRequestParser.instance(OptionMap.EMPTY).handle(ByteBuffer.wrap(in), context, result);
    +        Assert.assertEquals("/path/..", result.getRequestURI());
    +        Assert.assertEquals("/path/..", result.getRequestPath());
    +        Assert.assertEquals("/path/..", result.getRelativePath());
    +        Assert.assertEquals("/", result.getQueryString());
    +
    +        in = "GET /path/..~/ HTTP/1.1\r\n\r\n".getBytes();
    +        context = new ParseState(10);
    +        result = new HttpServerExchange(null);
    +        HttpRequestParser.instance(OptionMap.EMPTY).handle(ByteBuffer.wrap(in), context, result);
    +        Assert.assertEquals("/path/..~/", result.getRequestURI());
    +        Assert.assertEquals("/path/..~/", result.getRequestPath());
    +        Assert.assertEquals("/path/..~/", result.getRelativePath());
    +        Assert.assertEquals("", result.getQueryString());
    +    }
    +
     
         private void runTest(final byte[] in) throws BadRequestException {
             runTest(in, "some value");
    
7d388c5aae9b

Merge pull request #1557 from fl4via/UNDERTOW-2336

https://github.com/undertow-io/undertowFlavia RainoneFeb 21, 2024via ghsa
1 file changed · +61 21
  • core/src/main/java/io/undertow/conduits/WriteTimeoutStreamSinkConduit.java+61 21 modified
    @@ -22,8 +22,8 @@
     import io.undertow.UndertowOptions;
     import io.undertow.server.OpenListener;
     import io.undertow.util.WorkerUtils;
    -
     import org.xnio.Buffers;
    +import org.xnio.ChannelListener;
     import org.xnio.ChannelListeners;
     import org.xnio.IoUtils;
     import org.xnio.Options;
    @@ -47,7 +47,7 @@
      */
     public final class WriteTimeoutStreamSinkConduit extends AbstractStreamSinkConduit<StreamSinkConduit> {
     
    -    private XnioExecutor.Key handle;
    +    private volatile XnioExecutor.Key handle;
         private final StreamConnection connection;
         private volatile long expireTime = -1;
         private final OpenListener openListener;
    @@ -82,6 +82,16 @@ public WriteTimeoutStreamSinkConduit(final StreamSinkConduit delegate, StreamCon
             super(delegate);
             this.connection = connection;
             this.openListener = openListener;
    +        this.connection.getCloseSetter().set((ChannelListener<StreamConnection>) channel -> {
    +            if (handle != null) {
    +                synchronized (WriteTimeoutStreamSinkConduit.this) {
    +                    if (handle != null) {
    +                        handle.remove();
    +                        handle = null;
    +                    }
    +                }
    +            }
    +        });
         }
     
         private void handleWriteTimeout(final long ret) throws IOException {
    @@ -124,10 +134,14 @@ public long write(final ByteBuffer[] srcs, final int offset, final int length) t
         public int writeFinal(ByteBuffer src) throws IOException {
             int ret = super.writeFinal(src);
             handleWriteTimeout(ret);
    -        if(!src.hasRemaining()) {
    -            if(handle != null) {
    -                handle.remove();
    -                handle = null;
    +        if (!src.hasRemaining()) {
    +            if (handle != null) {
    +                synchronized (this) {
    +                    if (handle != null) {
    +                        handle.remove();
    +                        handle = null;
    +                    }
    +                }
                 }
             }
             return ret;
    @@ -137,10 +151,14 @@ public int writeFinal(ByteBuffer src) throws IOException {
         public long writeFinal(ByteBuffer[] srcs, int offset, int length) throws IOException {
             long ret = super.writeFinal(srcs, offset, length);
             handleWriteTimeout(ret);
    -        if(!Buffers.hasRemaining(srcs)) {
    -            if(handle != null) {
    -                handle.remove();
    -                handle = null;
    +        if (!Buffers.hasRemaining(srcs)) {
    +            if (handle != null) {
    +                synchronized (this) {
    +                    if (handle != null) {
    +                        handle.remove();
    +                        handle = null;
    +                    }
    +                }
                 }
             }
             return ret;
    @@ -200,19 +218,33 @@ private Integer getTimeout() {
     
         @Override
         public void terminateWrites() throws IOException {
    -        super.terminateWrites();
    -        if(handle != null) {
    -            handle.remove();
    -            handle = null;
    +        try {
    +            super.terminateWrites();
    +        } finally {
    +            if(handle != null) {
    +                synchronized (this) {
    +                    if (this.handle != null) {
    +                        handle.remove();
    +                        handle = null;
    +                    }
    +                }
    +            }
             }
         }
     
         @Override
         public void truncateWrites() throws IOException {
    -        super.truncateWrites();
    -        if(handle != null) {
    -            handle.remove();
    -            handle = null;
    +        try {
    +            super.truncateWrites();
    +        } finally {
    +            if (handle != null) {
    +                synchronized (this) {
    +                    if (this.handle != null) {
    +                        handle.remove();
    +                        handle = null;
    +                    }
    +                }
    +            }
             }
         }
     
    @@ -233,8 +265,12 @@ public void suspendWrites() {
     
             XnioExecutor.Key handle = this.handle;
             if(handle != null) {
    -            handle.remove();
    -            this.handle = null;
    +            synchronized (this) {
    +                if (this.handle != null) {
    +                    handle.remove();
    +                    this.handle = null;
    +                }
    +            }
             }
         }
     
    @@ -253,7 +289,11 @@ private void handleResumeTimeout() {
             expireTime = currentTime + timeout;
             XnioExecutor.Key key = handle;
             if (key == null) {
    -            handle = connection.getIoThread().executeAfter(timeoutCommand, timeout, TimeUnit.MILLISECONDS);
    +            synchronized (this) {
    +                if (handle == null) {
    +                    handle = connection.getIoThread().executeAfter(timeoutCommand, timeout, TimeUnit.MILLISECONDS);
    +                }
    +            }
             }
         }
     }
    

Vulnerability mechanics

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

References

20

News mentions

0

No linked articles in our index yet.