Skip to content

Commit 40bb331

Browse files
TomasHofmanfl4via
authored andcommitted
[UNDERTOW-2339] CVE-2024-1459 Path segment "/..;" should not be treated as "/.."
Proxies such as httpd proxy do not resolve the path segment "/..;/" to be a double dot segment, so they would pass such request path unchanged to target server. Undertow on the other hand resolves "/..;/" as double dot, which can cause essentially a path traversal problem, where client can request resources that should not be available to him per proxy configuration. Signed-off-by: Flavia Rainone <[email protected]>
1 parent 88df48a commit 40bb331

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
@@ -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)