Skip to content

Commit 9947d51

Browse files
committed
fix(static): prevent backslash-based path traversal in resolveDotSegments
normalize `\` to `/` before resolving dot segments to block traversal via `/..\\etc\\passwd` and `/%5c` encoded variants. adds test coverage for blocked traversal paths and allowed dot-containing filenames.
1 parent 99ec3a2 commit 9947d51

File tree

3 files changed

+37
-1
lines changed

3 files changed

+37
-1
lines changed

src/utils/internal/path.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ export function resolveDotSegments(path: string): string {
5656
if (!path.includes(".")) {
5757
return path;
5858
}
59-
const segments = path.split("/");
59+
// Normalize backslashes to forward slashes to prevent traversal via `\`
60+
const segments = path.replaceAll("\\", "/").split("/");
6061
const resolved: string[] = [];
6162
for (const segment of segments) {
6263
if (segment === "..") {

standard-context

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit 1db9a051ce1c2014a6ca5d0d2f3e569f9f4b05cc

test/static.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,40 @@ describeMatrix("serve static", (t, { it, expect }) => {
111111
expect(text).not.toContain("..");
112112
expect(text).toMatch(/^asset:\/etc\/passwd/);
113113
});
114+
115+
it("blocks path traversal attempts", async () => {
116+
const blocked = [
117+
"/../etc/passwd",
118+
"/%2e%2e/",
119+
"/%2E%2E/",
120+
"/assets/../../etc/passwd",
121+
"/assets/..",
122+
"/..\\etc\\passwd",
123+
"/..%5c..%5cetc%5cpasswd",
124+
"/..",
125+
];
126+
for (const path of blocked) {
127+
const res = await t.fetch(path);
128+
const text = await res.text();
129+
expect(text).not.toContain("..");
130+
}
131+
});
132+
133+
it("allows legitimate paths with dots", async () => {
134+
const allowed = [
135+
"/_...grid_123.js",
136+
"/file..name.js",
137+
"/.hidden",
138+
"/assets/file.txt",
139+
"/...test/file.js",
140+
];
141+
for (const path of allowed) {
142+
const res = await t.fetch(path);
143+
expect(res.status).toEqual(200);
144+
const text = await res.text();
145+
expect(text).toContain("asset:");
146+
}
147+
});
114148
});
115149

116150
describeMatrix("serve static with fallthrough", (t, { it, expect }) => {

0 commit comments

Comments
 (0)