Skip to content

Commit 6950c14

Browse files
authored
Fix: Path traversal vulnerability in StaticFileHandler (#65)
* Prevent path traversal and sanitize URI in StaticFileHandler. * Add test for path traversal protection and support 403 responses. * Add tests for URI sanitization and handling of encoded/special URLs. * Add test for null byte injection prevention in StaticFileHandler * Improve StaticFileHandler path traversal handling and test coverage * Improve URI sanitization and add test for 404 response handling in StaticFileHandler
1 parent 5c80eaa commit 6950c14

3 files changed

Lines changed: 75 additions & 19 deletions

File tree

src/main/java/org/example/StaticFileHandler.java

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,39 +23,45 @@ public StaticFileHandler(String webRoot) {
2323
}
2424

2525
private void handleGetRequest(String uri) throws IOException {
26-
// Security: Prevent path traversal attacks (e.g. GET /../../etc/passwd)
26+
// Sanitize URI
27+
int q = uri.indexOf('?');
28+
if (q >= 0) uri = uri.substring(0, q);
29+
int h = uri.indexOf('#');
30+
if (h >= 0) uri = uri.substring(0, h);
31+
uri = uri.replace("\0", "");
32+
if (uri.startsWith("/")) uri = uri.substring(1);
33+
34+
// Path traversal check
2735
File root = new File(WEB_ROOT).getCanonicalFile();
2836
File file = new File(root, uri).getCanonicalFile();
29-
3037
if (!file.toPath().startsWith(root.toPath())) {
31-
fileBytes = "403 Forbidden".getBytes();
38+
fileBytes = "403 Forbidden".getBytes(java.nio.charset.StandardCharsets.UTF_8);
3239
statusCode = 403;
3340
return;
3441
}
3542

36-
if (file.exists()) {
43+
// Read file
44+
if (file.isFile()) {
3745
fileBytes = Files.readAllBytes(file.toPath());
3846
statusCode = 200;
3947
} else {
4048
File errorFile = new File(WEB_ROOT, "pageNotFound.html");
41-
if (errorFile.exists()) {
49+
if (errorFile.isFile()) {
4250
fileBytes = Files.readAllBytes(errorFile.toPath());
4351
} else {
44-
fileBytes = "404 Not Found".getBytes();
52+
fileBytes = "404 Not Found".getBytes(java.nio.charset.StandardCharsets.UTF_8);
4553
}
4654
statusCode = 404;
4755
}
4856
}
4957

5058
public void sendGetRequest(OutputStream outputStream, String uri) throws IOException {
5159
handleGetRequest(uri);
52-
5360
HttpResponseBuilder response = new HttpResponseBuilder();
5461
response.setStatusCode(statusCode);
5562
// Use MimeTypeDetector instead of hardcoded text/html
5663
response.setContentTypeFromFilename(uri);
5764
response.setBody(fileBytes);
58-
5965
outputStream.write(response.build());
6066
outputStream.flush();
6167
}

src/main/java/org/example/http/HttpResponseBuilder.java

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,15 @@ public class HttpResponseBuilder {
3636
public void setStatusCode(int statusCode) {
3737
this.statusCode = statusCode;
3838
}
39+
3940
public void setBody(String body) {
4041
this.body = body != null ? body : "";
41-
this.bytebody = null; // Clear byte body when setting string body
42+
this.bytebody = null;
4243
}
4344

4445
public void setBody(byte[] body) {
4546
this.bytebody = body;
46-
this.body = ""; // Clear string body when setting byte body
47+
this.body = "";
4748
}
4849

4950
public void setHeaders(Map<String, String> headers) {
@@ -65,7 +66,6 @@ public void setContentTypeFromFilename(String filename) {
6566
* @return Complete HTTP response (headers + body) as byte[]
6667
*/
6768
public byte[] build() {
68-
// Determine content body and length
6969
byte[] contentBody;
7070
int contentLength;
7171

@@ -77,37 +77,29 @@ public byte[] build() {
7777
contentLength = contentBody.length;
7878
}
7979

80-
// Build headers as String
8180
StringBuilder headerBuilder = new StringBuilder();
8281

83-
// Status line
8482
String reason = REASON_PHRASES.getOrDefault(statusCode, "");
8583
headerBuilder.append(PROTOCOL).append(" ").append(statusCode);
8684
if (!reason.isEmpty()) {
8785
headerBuilder.append(" ").append(reason);
8886
}
8987
headerBuilder.append(CRLF);
9088

91-
// User-defined headers
9289
headers.forEach((k, v) -> headerBuilder.append(k).append(": ").append(v).append(CRLF));
9390

94-
// Auto-append Content-Length if not set.
9591
if (!headers.containsKey("Content-Length")) {
9692
headerBuilder.append("Content-Length: ").append(contentLength).append(CRLF);
9793
}
9894

99-
// Auto-append Connection if not set.
10095
if (!headers.containsKey("Connection")) {
10196
headerBuilder.append("Connection: close").append(CRLF);
10297
}
10398

104-
// Blank line before body
10599
headerBuilder.append(CRLF);
106100

107-
// Convert headers to bytes
108101
byte[] headerBytes = headerBuilder.toString().getBytes(StandardCharsets.UTF_8);
109102

110-
// Combine headers + body into single byte array
111103
byte[] response = new byte[headerBytes.length + contentBody.length];
112104
System.arraycopy(headerBytes, 0, response, 0, headerBytes.length);
113105
System.arraycopy(contentBody, 0, response, headerBytes.length, contentBody.length);

src/test/java/org/example/StaticFileHandlerTest.java

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import org.junit.jupiter.api.Test;
44
import org.junit.jupiter.api.io.TempDir;
5+
import org.junit.jupiter.params.ParameterizedTest;
6+
import org.junit.jupiter.params.provider.CsvSource;
57

68
import java.io.ByteArrayOutputStream;
79
import java.io.IOException;
@@ -76,4 +78,60 @@ void test_file_that_does_not_exists_should_return_404() throws IOException {
7678

7779
}
7880

81+
@Test
82+
void test_path_traversal_should_return_403() throws IOException {
83+
// Arrange
84+
Path secret = tempDir.resolve("secret.txt");
85+
Files.writeString(secret,"TOP SECRET");
86+
Path webRoot = tempDir.resolve("www");
87+
Files.createDirectories(webRoot);
88+
StaticFileHandler handler = new StaticFileHandler(webRoot.toString());
89+
ByteArrayOutputStream fakeOutput = new ByteArrayOutputStream();
90+
91+
// Act
92+
handler.sendGetRequest(fakeOutput, "../secret.txt");
93+
94+
// Assert
95+
String response = fakeOutput.toString();
96+
assertFalse(response.contains("TOP SECRET"));
97+
assertTrue(response.contains("HTTP/1.1 403 Forbidden"));
98+
}
99+
100+
@ParameterizedTest
101+
@CsvSource({
102+
"index.html?foo=bar",
103+
"index.html#section",
104+
"/index.html"
105+
})
106+
void sanitized_uris_should_return_200(String uri) throws IOException {
107+
// Arrange
108+
Path webRoot = tempDir.resolve("www");
109+
Files.createDirectories(webRoot);
110+
Files.writeString(webRoot.resolve("index.html"), "Hello");
111+
StaticFileHandler handler = new StaticFileHandler(webRoot.toString());
112+
ByteArrayOutputStream out = new ByteArrayOutputStream();
113+
114+
// Act
115+
handler.sendGetRequest(out, uri);
116+
117+
// Assert
118+
assertTrue(out.toString().contains("HTTP/1.1 200 OK"));
119+
}
120+
121+
@Test
122+
void null_byte_injection_should_not_return_200() throws IOException {
123+
// Arrange
124+
Path webRoot = tempDir.resolve("www");
125+
Files.createDirectories(webRoot);
126+
StaticFileHandler handler = new StaticFileHandler(webRoot.toString());
127+
ByteArrayOutputStream out = new ByteArrayOutputStream();
128+
129+
// Act
130+
handler.sendGetRequest(out, "index.html\0../../etc/passwd");
131+
132+
// Assert
133+
String response = out.toString();
134+
assertFalse(response.contains("HTTP/1.1 200 OK"));
135+
assertTrue(response.contains("HTTP/1.1 404 Not Found"));
136+
}
79137
}

0 commit comments

Comments
 (0)