Merge pull request #1420 from lightpanda-io/resolve_fix

Handle URL.resolve with path traversal as part of the filename
This commit is contained in:
Karl Seguin
2026-01-28 06:46:35 +08:00
committed by GitHub

View File

@@ -77,8 +77,9 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime
} }
// trailing space so that we always have space to append the null terminator // trailing space so that we always have space to append the null terminator
var out = try std.mem.join(allocator, "", &.{ normalized_base, "/", path, " " }); // and so that we can compare the next two characters without needing to length check
const end = out.len - 1; var out = try std.mem.join(allocator, "", &.{ normalized_base, "/", path, " " });
const end = out.len - 2;
const path_marker = path_start + 1; const path_marker = path_start + 1;
@@ -88,33 +89,39 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime
var in_i: usize = 0; var in_i: usize = 0;
var out_i: usize = 0; var out_i: usize = 0;
while (in_i < end) { while (in_i < end) {
if (std.mem.startsWith(u8, out[in_i..], "./")) { if (out[in_i] == '.' and (out_i == 0 or out[out_i - 1] == '/')) {
in_i += 2; if (out[in_i + 1] == '/') { // always safe, because we added a whitespace
continue; // /./
} in_i += 2;
continue;
if (std.mem.startsWith(u8, out[in_i..], "../")) { }
lp.assert(out[out_i - 1] == '/', "URL.resolve", .{ .out = out }); if (out[in_i + 1] == '.' and out[in_i + 2] == '/') { // always safe, because we added two whitespaces
// /../
if (out_i > path_marker) { if (out_i > path_marker) {
// go back before the / // go back before the /
out_i -= 2; out_i -= 2;
while (out_i > 1 and out[out_i - 1] != '/') { while (out_i > 1 and out[out_i - 1] != '/') {
out_i -= 1; out_i -= 1;
} }
} else { } else {
// if out_i == path_marker, than we've reached the start of // if out_i == path_marker, than we've reached the start of
// the path. We can't ../ any more. E.g.: // the path. We can't ../ any more. E.g.:
// http://www.example.com/../hello. // http://www.example.com/../hello.
// You might think that's an error, but, at least with // You might think that's an error, but, at least with
// new URL('../hello', 'http://www.example.com/') // new URL('../hello', 'http://www.example.com/')
// it just ignores the extra ../ // it just ignores the extra ../
}
in_i += 3;
continue;
}
if (in_i == end - 1) {
// ignore trailing dot
break;
} }
in_i += 3;
continue;
} }
out[out_i] = out[in_i]; const c = out[in_i];
out[out_i] = c;
in_i += 1; in_i += 1;
out_i += 1; out_i += 1;
} }
@@ -542,6 +549,21 @@ test "URL: resolve" {
}; };
const cases = [_]Case{ const cases = [_]Case{
.{
.base = "https://example/dir",
.path = "abc../test",
.expected = "https://example/abc../test",
},
.{
.base = "https://example/dir",
.path = "abc.",
.expected = "https://example/abc.",
},
.{
.base = "https://example/dir",
.path = "abc/.",
.expected = "https://example/abc/",
},
.{ .{
.base = "https://example/xyz/abc/123", .base = "https://example/xyz/abc/123",
.path = "something.js", .path = "something.js",