Skip to content

Commit 3cdb104

Browse files
authored
Merge pull request #1559 from fl4via/2.2.x_backport_bug_fixes
[UNDERTOW-2280][UNDERTOW-2336][UNDERTOW-2339] CVE-2023-5379 CVE-2024-1459 CVE-2024-1635 Backport bug fixes
2 parents 883bd50 + e824766 commit 3cdb104

File tree

5 files changed

+126
-23
lines changed

5 files changed

+126
-23
lines changed

core/src/main/java/io/undertow/conduits/WriteTimeoutStreamSinkConduit.java

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
import io.undertow.UndertowOptions;
2323
import io.undertow.server.OpenListener;
2424
import io.undertow.util.WorkerUtils;
25-
2625
import org.xnio.Buffers;
26+
import org.xnio.ChannelListener;
2727
import org.xnio.ChannelListeners;
2828
import org.xnio.IoUtils;
2929
import org.xnio.Options;
@@ -47,7 +47,7 @@
4747
*/
4848
public final class WriteTimeoutStreamSinkConduit extends AbstractStreamSinkConduit<StreamSinkConduit> {
4949

50-
private XnioExecutor.Key handle;
50+
private volatile XnioExecutor.Key handle;
5151
private final StreamConnection connection;
5252
private volatile long expireTime = -1;
5353
private final OpenListener openListener;
@@ -82,6 +82,16 @@ public WriteTimeoutStreamSinkConduit(final StreamSinkConduit delegate, StreamCon
8282
super(delegate);
8383
this.connection = connection;
8484
this.openListener = openListener;
85+
this.connection.getCloseSetter().set((ChannelListener<StreamConnection>) channel -> {
86+
if (handle != null) {
87+
synchronized (WriteTimeoutStreamSinkConduit.this) {
88+
if (handle != null) {
89+
handle.remove();
90+
handle = null;
91+
}
92+
}
93+
}
94+
});
8595
}
8696

8797
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
124134
public int writeFinal(ByteBuffer src) throws IOException {
125135
int ret = super.writeFinal(src);
126136
handleWriteTimeout(ret);
127-
if(!src.hasRemaining()) {
128-
if(handle != null) {
129-
handle.remove();
130-
handle = null;
137+
if (!src.hasRemaining()) {
138+
if (handle != null) {
139+
synchronized (this) {
140+
if (handle != null) {
141+
handle.remove();
142+
handle = null;
143+
}
144+
}
131145
}
132146
}
133147
return ret;
@@ -137,10 +151,14 @@ public int writeFinal(ByteBuffer src) throws IOException {
137151
public long writeFinal(ByteBuffer[] srcs, int offset, int length) throws IOException {
138152
long ret = super.writeFinal(srcs, offset, length);
139153
handleWriteTimeout(ret);
140-
if(!Buffers.hasRemaining(srcs)) {
141-
if(handle != null) {
142-
handle.remove();
143-
handle = null;
154+
if (!Buffers.hasRemaining(srcs)) {
155+
if (handle != null) {
156+
synchronized (this) {
157+
if (handle != null) {
158+
handle.remove();
159+
handle = null;
160+
}
161+
}
144162
}
145163
}
146164
return ret;
@@ -200,19 +218,33 @@ private Integer getTimeout() {
200218

201219
@Override
202220
public void terminateWrites() throws IOException {
203-
super.terminateWrites();
204-
if(handle != null) {
205-
handle.remove();
206-
handle = null;
221+
try {
222+
super.terminateWrites();
223+
} finally {
224+
if(handle != null) {
225+
synchronized (this) {
226+
if (this.handle != null) {
227+
handle.remove();
228+
handle = null;
229+
}
230+
}
231+
}
207232
}
208233
}
209234

210235
@Override
211236
public void truncateWrites() throws IOException {
212-
super.truncateWrites();
213-
if(handle != null) {
214-
handle.remove();
215-
handle = null;
237+
try {
238+
super.truncateWrites();
239+
} finally {
240+
if (handle != null) {
241+
synchronized (this) {
242+
if (this.handle != null) {
243+
handle.remove();
244+
handle = null;
245+
}
246+
}
247+
}
216248
}
217249
}
218250

@@ -233,8 +265,12 @@ public void suspendWrites() {
233265

234266
XnioExecutor.Key handle = this.handle;
235267
if(handle != null) {
236-
handle.remove();
237-
this.handle = null;
268+
synchronized (this) {
269+
if (this.handle != null) {
270+
handle.remove();
271+
this.handle = null;
272+
}
273+
}
238274
}
239275
}
240276

@@ -253,7 +289,11 @@ private void handleResumeTimeout() {
253289
expireTime = currentTime + timeout;
254290
XnioExecutor.Key key = handle;
255291
if (key == null) {
256-
handle = connection.getIoThread().executeAfter(timeoutCommand, timeout, TimeUnit.MILLISECONDS);
292+
synchronized (this) {
293+
if (handle == null) {
294+
handle = connection.getIoThread().executeAfter(timeoutCommand, timeout, TimeUnit.MILLISECONDS);
295+
}
296+
}
257297
}
258298
}
259299
}

core/src/main/java/io/undertow/server/protocol/ajp/AjpReadListener.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
package io.undertow.server.protocol.ajp;
2020

2121
import io.undertow.UndertowLogger;
22+
import io.undertow.UndertowMessages;
2223
import io.undertow.UndertowOptions;
2324
import io.undertow.conduits.ConduitListener;
2425
import io.undertow.conduits.EmptyStreamSourceConduit;
@@ -165,8 +166,7 @@ public void handleEvent(final StreamSourceChannel channel) {
165166
}
166167
if (read > maxRequestSize) {
167168
UndertowLogger.REQUEST_LOGGER.requestHeaderWasTooLarge(connection.getPeerAddress(), maxRequestSize);
168-
safeClose(connection);
169-
return;
169+
throw UndertowMessages.MESSAGES.badRequest();
170170
}
171171
} while (!state.isComplete());
172172

core/src/main/java/io/undertow/server/protocol/http/HttpRequestParser.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,10 @@ private void handleStateful(ByteBuffer buffer, ParseState currentState, HttpServ
372372
private static final int IN_PATH = 4;
373373
private static final int HOST_DONE = 5;
374374

375+
private static final int PATH_SEGMENT_START = 0;
376+
private static final int PATH_DOT_SEGMENT = 1;
377+
private static final int PATH_NON_DOT_SEGMENT = 2;
378+
375379
/**
376380
* Parses a path value
377381
*
@@ -387,6 +391,8 @@ final void handlePath(ByteBuffer buffer, ParseState state, HttpServerExchange ex
387391
int canonicalPathStart = state.pos;
388392
boolean urlDecodeRequired = state.urlDecodeRequired;
389393

394+
int pathSubState = 0;
395+
390396
while (buffer.hasRemaining()) {
391397
char next = (char) (buffer.get() & 0xFF);
392398
if(!allowUnescapedCharactersInUrl && !ALLOWED_TARGET_CHARACTER[next]) {
@@ -410,6 +416,11 @@ final void handlePath(ByteBuffer buffer, ParseState state, HttpServerExchange ex
410416
state.urlDecodeRequired = urlDecodeRequired;
411417
// store at canonical path the partial path parsed up until here
412418
state.canonicalPath.append(stringBuilder.substring(canonicalPathStart));
419+
if (parseState == IN_PATH && pathSubState == PATH_DOT_SEGMENT) {
420+
// Inside a dot-segment (".", ".."), we don't want to allow removal of the ';' character from
421+
// the path. This is to avoid path traversal issues - "/..;" should not be treated as "/..".
422+
state.canonicalPath.append(";");
423+
}
413424
state.stringBuilder.append(";");
414425
// set position to end of path (possibly start of parameter name)
415426
state.pos = state.stringBuilder.length();
@@ -443,6 +454,18 @@ final void handlePath(ByteBuffer buffer, ParseState state, HttpServerExchange ex
443454
} else if (next == '/' && parseState != HOST_DONE) {
444455
parseState = IN_PATH;
445456
}
457+
458+
// This is helper state that tracks if the parser is currently in a path dot-segment (".", "..") or not.
459+
if (parseState == IN_PATH) {
460+
if (next == '/') {
461+
pathSubState = PATH_SEGMENT_START;
462+
} else if (next == '.' && (pathSubState == PATH_SEGMENT_START || pathSubState == PATH_DOT_SEGMENT)) {
463+
pathSubState = PATH_DOT_SEGMENT;
464+
} else {
465+
pathSubState = PATH_NON_DOT_SEGMENT;
466+
}
467+
}
468+
446469
stringBuilder.append(next);
447470
}
448471

core/src/main/java/io/undertow/server/protocol/http/ParseState.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ public void reset() {
142142
this.leftOver = 0;
143143
this.urlDecodeRequired = false;
144144
this.stringBuilder.setLength(0);
145+
this.canonicalPath.setLength(0);
145146
this.nextHeader = null;
146147
this.nextQueryParam = null;
147148
this.mapCount = 0;

core/src/test/java/io/undertow/server/protocol/http/SimpleParserTestCase.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,45 @@ public void testNonEncodedAsciiCharactersExplicitlyAllowed() throws UnsupportedE
676676
Assert.assertEquals("/bår", result.getRequestURI()); //not decoded
677677
}
678678

679+
@Test
680+
public void testDirectoryTraversal() throws Exception {
681+
byte[] in = "GET /path/..;/ HTTP/1.1\r\n\r\n".getBytes();
682+
ParseState context = new ParseState(10);
683+
HttpServerExchange result = new HttpServerExchange(null);
684+
HttpRequestParser.instance(OptionMap.EMPTY).handle(ByteBuffer.wrap(in), context, result);
685+
Assert.assertEquals("/path/..;/", result.getRequestURI());
686+
Assert.assertEquals("/path/..;/", result.getRequestPath());
687+
Assert.assertEquals("/path/..;/", result.getRelativePath());
688+
Assert.assertEquals("", result.getQueryString());
689+
690+
in = "GET /path/../ HTTP/1.1\r\n\r\n".getBytes();
691+
context = new ParseState(10);
692+
result = new HttpServerExchange(null);
693+
HttpRequestParser.instance(OptionMap.EMPTY).handle(ByteBuffer.wrap(in), context, result);
694+
Assert.assertEquals("/path/../", result.getRequestURI());
695+
Assert.assertEquals("/path/../", result.getRequestPath());
696+
Assert.assertEquals("/path/../", result.getRelativePath());
697+
Assert.assertEquals("", result.getQueryString());
698+
699+
in = "GET /path/..?/ HTTP/1.1\r\n\r\n".getBytes();
700+
context = new ParseState(10);
701+
result = new HttpServerExchange(null);
702+
HttpRequestParser.instance(OptionMap.EMPTY).handle(ByteBuffer.wrap(in), context, result);
703+
Assert.assertEquals("/path/..", result.getRequestURI());
704+
Assert.assertEquals("/path/..", result.getRequestPath());
705+
Assert.assertEquals("/path/..", result.getRelativePath());
706+
Assert.assertEquals("/", result.getQueryString());
707+
708+
in = "GET /path/..~/ HTTP/1.1\r\n\r\n".getBytes();
709+
context = new ParseState(10);
710+
result = new HttpServerExchange(null);
711+
HttpRequestParser.instance(OptionMap.EMPTY).handle(ByteBuffer.wrap(in), context, result);
712+
Assert.assertEquals("/path/..~/", result.getRequestURI());
713+
Assert.assertEquals("/path/..~/", result.getRequestPath());
714+
Assert.assertEquals("/path/..~/", result.getRelativePath());
715+
Assert.assertEquals("", result.getQueryString());
716+
}
717+
679718

680719
private void runTest(final byte[] in) throws BadRequestException {
681720
runTest(in, "some value");

0 commit comments

Comments
 (0)