Skip to content

Commit 54f3e43

Browse files
authored
Merge pull request #1556 from fl4via/UNDERTOW-2339
[UNDERTOW-2339] CVE-2024-1459 Path segment "/..;" should not be treated as "/.."
2 parents 7d388c5 + 9b7c503 commit 54f3e43

File tree

3 files changed

+63
-0
lines changed

3 files changed

+63
-0
lines changed

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
@@ -388,6 +388,10 @@ private void sanitazeListTypeHeaders(final HttpServerExchange builder) {
388388
private static final int IN_PATH = 4;
389389
private static final int HOST_DONE = 5;
390390

391+
private static final int PATH_SEGMENT_START = 0;
392+
private static final int PATH_DOT_SEGMENT = 1;
393+
private static final int PATH_NON_DOT_SEGMENT = 2;
394+
391395
/**
392396
* Parses a path value
393397
*
@@ -403,6 +407,8 @@ final void handlePath(ByteBuffer buffer, ParseState state, HttpServerExchange ex
403407
int canonicalPathStart = state.pos;
404408
boolean urlDecodeRequired = state.urlDecodeRequired;
405409

410+
int pathSubState = 0;
411+
406412
while (buffer.hasRemaining()) {
407413
char next = (char) (buffer.get() & 0xFF);
408414
if(!allowUnescapedCharactersInUrl && !ALLOWED_TARGET_CHARACTER[next]) {
@@ -426,6 +432,11 @@ final void handlePath(ByteBuffer buffer, ParseState state, HttpServerExchange ex
426432
state.urlDecodeRequired = urlDecodeRequired;
427433
// store at canonical path the partial path parsed up until here
428434
state.canonicalPath.append(stringBuilder.substring(canonicalPathStart));
435+
if (parseState == IN_PATH && pathSubState == PATH_DOT_SEGMENT) {
436+
// Inside a dot-segment (".", ".."), we don't want to allow removal of the ';' character from
437+
// the path. This is to avoid path traversal issues - "/..;" should not be treated as "/..".
438+
state.canonicalPath.append(";");
439+
}
429440
state.stringBuilder.append(";");
430441
// set position to end of path (possibly start of parameter name)
431442
state.pos = state.stringBuilder.length();
@@ -459,6 +470,18 @@ final void handlePath(ByteBuffer buffer, ParseState state, HttpServerExchange ex
459470
} else if (next == '/' && parseState != HOST_DONE) {
460471
parseState = IN_PATH;
461472
}
473+
474+
// This is helper state that tracks if the parser is currently in a path dot-segment (".", "..") or not.
475+
if (parseState == IN_PATH) {
476+
if (next == '/') {
477+
pathSubState = PATH_SEGMENT_START;
478+
} else if (next == '.' && (pathSubState == PATH_SEGMENT_START || pathSubState == PATH_DOT_SEGMENT)) {
479+
pathSubState = PATH_DOT_SEGMENT;
480+
} else {
481+
pathSubState = PATH_NON_DOT_SEGMENT;
482+
}
483+
}
484+
462485
stringBuilder.append(next);
463486
}
464487

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)