Undertow: out-of-memory error after several closed connections with wildfly-http-client protocol
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.
| Package | Affected versions | Patched versions |
|---|---|---|
io.undertow:undertow-coreMaven | >= 2.3.0.Final, < 2.3.12.Final | 2.3.12.Final |
io.undertow:undertow-coreMaven | < 2.2.31.Final | 2.2.31.Final |
Affected products
2Patches
23cdb104e225fMerge pull request #1559 from fl4via/2.2.x_backport_bug_fixes
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");
7d388c5aae9bMerge pull request #1557 from fl4via/UNDERTOW-2336
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- access.redhat.com/errata/RHSA-2024:1674ghsavendor-advisoryx_refsource_REDHATWEB
- access.redhat.com/errata/RHSA-2024:1675ghsavendor-advisoryx_refsource_REDHATWEB
- access.redhat.com/errata/RHSA-2024:1676ghsavendor-advisoryx_refsource_REDHATWEB
- access.redhat.com/errata/RHSA-2024:1677ghsavendor-advisoryx_refsource_REDHATWEB
- access.redhat.com/errata/RHSA-2024:1860ghsavendor-advisoryx_refsource_REDHATWEB
- access.redhat.com/errata/RHSA-2024:1861ghsavendor-advisoryx_refsource_REDHATWEB
- access.redhat.com/errata/RHSA-2024:1862ghsavendor-advisoryx_refsource_REDHATWEB
- access.redhat.com/errata/RHSA-2024:1864ghsavendor-advisoryx_refsource_REDHATWEB
- access.redhat.com/errata/RHSA-2024:1866ghsavendor-advisoryx_refsource_REDHATWEB
- access.redhat.com/errata/RHSA-2024:3354ghsavendor-advisoryx_refsource_REDHATWEB
- access.redhat.com/errata/RHSA-2024:4884ghsavendor-advisoryx_refsource_REDHATWEB
- access.redhat.com/errata/RHSA-2025:4226ghsavendor-advisoryx_refsource_REDHATWEB
- access.redhat.com/errata/RHSA-2025:9583mitrevendor-advisoryx_refsource_REDHAT
- github.com/advisories/GHSA-w6qf-42m7-vh68ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-1635ghsaADVISORY
- access.redhat.com/security/cve/CVE-2024-1635ghsavdb-entryx_refsource_REDHATWEB
- bugzilla.redhat.com/show_bug.cgighsaissue-trackingx_refsource_REDHATWEB
- github.com/undertow-io/undertow/commit/3cdb104e225f34547ce9fd6eb8799eb68e040f19ghsaWEB
- github.com/undertow-io/undertow/commit/7d388c5aae9b82afb63f24e3b6a2044838dfb4deghsaWEB
- security.netapp.com/advisory/ntap-20240322-0007ghsaWEB
News mentions
0No linked articles in our index yet.