-
Notifications
You must be signed in to change notification settings - Fork 38
Expand file tree
/
Copy pathHTTPContextTest.java
More file actions
238 lines (202 loc) · 8.28 KB
/
HTTPContextTest.java
File metadata and controls
238 lines (202 loc) · 8.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
/*
* Copyright (c) 2026, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific
* language governing permissions and limitations under the License.
*/
package io.fusionauth.http.server;
import java.io.IOException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
/**
* Tests for HTTPContext focusing on path traversal security and resource resolution.
* <p>
* These tests verify that HTTPContext properly prevents path traversal attacks as described in:
* - CVE-2019-19781 (Citrix path traversal)
* - Blog post: https://blog.dochia.dev/blog/http_edge_cases/
*
* @author FusionAuth
*/
public class HTTPContextTest {
private Path tempDir;
private HTTPContext context;
@BeforeMethod
public void setup() throws IOException {
// Create a temporary directory structure for testing
tempDir = Files.createTempDirectory("http-context-test");
// Create legitimate test files
Files.writeString(tempDir.resolve("index.html"), "<html>Index</html>");
Path cssDir = Files.createDirectory(tempDir.resolve("css"));
Files.writeString(cssDir.resolve("style.css"), "body { color: blue; }");
Path subDir = Files.createDirectory(tempDir.resolve("subdir"));
Files.writeString(subDir.resolve("file.txt"), "Legitimate file");
// Create file outside the baseDir to test traversal attempts
Path parentDir = tempDir.getParent();
Files.writeString(parentDir.resolve("secret.txt"), "Secret data");
context = new HTTPContext(tempDir);
}
@AfterMethod
public void teardown() throws IOException {
// Cleanup temp files
if (tempDir != null && Files.exists(tempDir)) {
Files.walk(tempDir)
.sorted((a, b) -> b.compareTo(a)) // Delete files before directories
.forEach(path -> {
try {
Files.deleteIfExists(path);
} catch (IOException e) {
// Ignore cleanup errors
}
});
}
// Cleanup secret file from parent
Path secretFile = tempDir.getParent().resolve("secret.txt");
Files.deleteIfExists(secretFile);
}
/**
* Test that legitimate file paths work correctly.
*/
@Test
public void testLegitimatePathsSucceed() throws Exception {
// Test root level file
URL indexUrl = context.getResource("index.html");
assertNotNull(indexUrl, "Should resolve index.html");
assertTrue(indexUrl.toString().contains("index.html"));
// Test subdirectory file
URL cssUrl = context.getResource("css/style.css");
assertNotNull(cssUrl, "Should resolve css/style.css");
assertTrue(cssUrl.toString().contains("style.css"));
// Test with leading slash (should be stripped)
URL slashUrl = context.getResource("/css/style.css");
assertNotNull(slashUrl, "Should resolve /css/style.css");
assertTrue(slashUrl.toString().contains("style.css"));
// Test nested path
URL subdirUrl = context.getResource("subdir/file.txt");
assertNotNull(subdirUrl, "Should resolve subdir/file.txt");
assertTrue(subdirUrl.toString().contains("file.txt"));
}
/**
* Test path traversal attack using ../ sequences (CVE-2019-19781 style).
* These attacks attempt to escape the baseDir and access parent directories.
*/
@Test
public void testPathTraversalAttacksBlocked() {
// Simple parent directory traversal
URL result1 = context.getResource("../secret.txt");
assertNull(result1, "Should block ../secret.txt");
// Multiple parent traversals
URL result2 = context.getResource("../../etc/passwd");
assertNull(result2, "Should block ../../etc/passwd");
// Traversal with valid path prefix
URL result3 = context.getResource("css/../../secret.txt");
assertNull(result3, "Should block css/../../secret.txt");
// Deep traversal
URL result4 = context.getResource("subdir/../../secret.txt");
assertNull(result4, "Should block subdir/../../secret.txt");
// Many parent directory references
URL result5 = context.getResource("../../../../../../../../../etc/passwd");
assertNull(result5, "Should block ../../../../../../../../../etc/passwd");
}
/**
* Test URL-encoded path traversal attacks.
* Attackers often URL-encode the ../ sequences to bypass naive filters.
*/
@Test
public void testUrlEncodedTraversalBlocked() {
// URL-encoded ../ is %2e%2e%2f
URL result1 = context.getResource("%2e%2e%2fsecret.txt");
assertNull(result1, "Should block URL-encoded traversal %2e%2e%2fsecret.txt");
URL result2 = context.getResource("%2e%2e%2f%2e%2e%2fsecret.txt");
assertNull(result2, "Should block %2e%2e%2f%2e%2e%2fsecret.txt");
// Mixed encoded and plain
URL result3 = context.getResource("css/%2e%2e%2f%2e%2e%2fsecret.txt");
assertNull(result3, "Should block css/%2e%2e%2f%2e%2e%2fsecret.txt");
}
/**
* Test that resolve() method also prevents path traversal.
*/
@Test
public void testResolvePathTraversalBlocked() {
// Simple parent directory traversal
Path result1 = context.resolve("../secret.txt");
assertNull(result1, "Should block ../secret.txt in resolve()");
// Multiple parent traversals
Path result2 = context.resolve("../../etc/passwd");
assertNull(result2, "Should block ../../etc/passwd in resolve()");
// Traversal with valid path prefix
Path result3 = context.resolve("css/../../secret.txt");
assertNull(result3, "Should block css/../../secret.txt in resolve()");
}
/**
* Test that resolve() works correctly for legitimate paths.
*/
@Test
public void testResolveLegitimatePathsSucceed() {
// Test root level file
Path indexPath = context.resolve("index.html");
assertNotNull(indexPath, "Should resolve index.html");
assertEquals(indexPath, tempDir.resolve("index.html"));
// Test subdirectory file
Path cssPath = context.resolve("css/style.css");
assertNotNull(cssPath, "Should resolve css/style.css");
assertEquals(cssPath, tempDir.resolve("css/style.css"));
// Test with leading slash
Path slashPath = context.resolve("/css/style.css");
assertNotNull(slashPath, "Should resolve /css/style.css");
assertEquals(slashPath, tempDir.resolve("css/style.css"));
}
/**
* Test edge case: path that goes down then up but stays within baseDir.
* For example: "subdir/../index.html" should resolve to "index.html"
*/
@Test
public void testNormalizedPathWithinBaseDirSucceeds() {
// This path traverses up but stays within baseDir after normalization
URL result = context.getResource("subdir/../index.html");
assertNotNull(result, "Should allow subdir/../index.html as it normalizes to index.html");
assertTrue(result.toString().contains("index.html"));
Path resolved = context.resolve("subdir/../index.html");
assertNotNull(resolved, "Should resolve subdir/../index.html");
assertEquals(resolved, tempDir.resolve("index.html"));
}
/**
* Test that non-existent files return null (not exceptions).
*/
@Test
public void testNonExistentFileReturnsNull() {
URL result = context.getResource("does-not-exist.txt");
assertNull(result);
}
/**
* Test attribute storage (not security related, but completeness).
*/
@Test
public void testAttributeStorage() {
context.setAttribute("test", "value");
assertEquals(context.getAttribute("test"), "value");
context.setAttribute("number", 42);
assertEquals(context.getAttribute("number"), 42);
Object removed = context.removeAttribute("test");
assertEquals(removed, "value");
assertNull(context.getAttribute("test"));
}
}