authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-03-27 01:12:30-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-03-27 01:27:38-07:00
log9ad6843b20edd864047ff7d8a9af935dd9dc3d48
tree5e71934940b6313b1369ec2875244ef29761801c
parent79b5433da713a1d60b8651c34a2459833900f4fa

std.tar.extract: sanitize path traversal

closes #31362

1 files changed, 54 insertions(+), 20 deletions(-)

lib/std/tar.zig+54-20
...@@ -588,8 +588,9 @@ pub const pipeToFileSystem = extract;...@@ -588,8 +588,9 @@ pub const pipeToFileSystem = extract;
588/// Ingests tar file from `reader`, populating file contents within `dir`. If588/// Ingests tar file from `reader`, populating file contents within `dir`. If
589/// any file would be extracted outside of `dir`, an error is return instead.589/// any file would be extracted outside of `dir`, an error is return instead.
590pub fn extract(io: Io, dir: Io.Dir, reader: *Io.Reader, options: ExtractOptions) !void {590pub fn extract(io: Io, dir: Io.Dir, reader: *Io.Reader, options: ExtractOptions) !void {
591 var file_name_buffer: [std.fs.max_path_bytes]u8 = undefined;591 var file_name_buffer: [Io.Dir.max_path_bytes]u8 = undefined;
592 var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined;592 var link_name_buffer: [Io.Dir.max_path_bytes]u8 = undefined;
593 var sanitize_buffer: [Io.Dir.max_path_bytes]u8 = undefined;
593 var file_contents_buffer: [1024]u8 = undefined;594 var file_contents_buffer: [1024]u8 = undefined;
594 var it: Iterator = .init(reader, .{595 var it: Iterator = .init(reader, .{
595 .file_name_buffer = &file_name_buffer,596 .file_name_buffer = &file_name_buffer,
...@@ -598,14 +599,15 @@ pub fn extract(io: Io, dir: Io.Dir, reader: *Io.Reader, options: ExtractOptions)...@@ -598,14 +599,15 @@ pub fn extract(io: Io, dir: Io.Dir, reader: *Io.Reader, options: ExtractOptions)
598 });599 });
599600
600 while (try it.next()) |file| {601 while (try it.next()) |file| {
601 const file_name = stripComponents(file.name, options.strip_components);602 const n = sanitizePath(&sanitize_buffer, file.name, options.strip_components) catch 0;
602 if (file_name.len == 0 and file.kind != .directory) {603 if (n == 0 and file.kind != .directory) {
603 const d = options.diagnostics orelse return error.TarComponentsOutsideStrippedPrefix;604 const d = options.diagnostics orelse return error.TarComponentsOutsideStrippedPrefix;
604 try d.errors.append(d.allocator, .{ .components_outside_stripped_prefix = .{605 try d.errors.append(d.allocator, .{ .components_outside_stripped_prefix = .{
605 .file_name = try d.allocator.dupe(u8, file.name),606 .file_name = try d.allocator.dupe(u8, file.name),
606 } });607 } });
607 continue;608 continue;
608 }609 }
610 const file_name = sanitize_buffer[0..n];
609 if (options.diagnostics) |d| {611 if (options.diagnostics) |d| {
610 try d.findRoot(file.kind, file_name);612 try d.findRoot(file.kind, file_name);
611 }613 }
...@@ -671,27 +673,59 @@ fn createDirAndSymlink(io: Io, dir: Io.Dir, link_name: []const u8, file_name: []...@@ -671,27 +673,59 @@ fn createDirAndSymlink(io: Io, dir: Io.Dir, link_name: []const u8, file_name: []
671 };673 };
672}674}
673675
674fn stripComponents(path: []const u8, count: u32) []const u8 {676fn sanitizePath(buffer: []u8, path: []const u8, strip_components: u32) error{Invalid}!usize {
677 if (path.len == 0 or path[0] == '/') return error.Invalid;
675 var i: usize = 0;678 var i: usize = 0;
676 var c = count;679 var c = strip_components;
677 while (c > 0) : (c -= 1) {680 var it = std.mem.tokenizeScalar(u8, path, '/');
678 if (std.mem.findScalarPos(u8, path, i, '/')) |pos| {681 while (it.next()) |component| {
679 i = pos + 1;682 if (std.mem.eql(u8, component, ".")) continue;
680 } else {683 if (std.mem.eql(u8, component, "..")) {
681 i = path.len;684 if (i == 0) return error.Invalid;
682 break;685 while (true) {
686 const ends_with_slash = buffer[i - 1] == '/';
687 i -= 1;
688 if (ends_with_slash or i == 0) break;
689 }
690 continue;
691 }
692 if (c > 0) {
693 c -= 1;
694 continue;
695 }
696 if (i > 0) {
697 buffer[i] = '/';
698 i += 1;
683 }699 }
700 @memcpy(buffer[i..][0..component.len], component);
701 i += component.len;
684 }702 }
685 return path[i..];703 if (c > 0) return error.Invalid;
704 return i;
705}
706
707fn testSanitizePath(expected: []const u8, input: []const u8, strip: u32) !void {
708 var buffer: [Io.Dir.max_path_bytes]u8 = undefined;
709 const result = buffer[0..try sanitizePath(&buffer, input, strip)];
710 try testing.expectEqualStrings(expected, result);
711}
712
713fn testSanitizePathError(expected: anyerror, input: []const u8, strip: u32) !void {
714 var buffer: [Io.Dir.max_path_bytes]u8 = undefined;
715 try testing.expectError(expected, sanitizePath(&buffer, input, strip));
686}716}
687717
688test stripComponents {718test sanitizePath {
689 const expectEqualStrings = testing.expectEqualStrings;719 try testSanitizePath("a/b/c", "a/b/c", 0);
690 try expectEqualStrings("a/b/c", stripComponents("a/b/c", 0));720 try testSanitizePath("a/b/c", "a/x/y/../../b/c", 0);
691 try expectEqualStrings("b/c", stripComponents("a/b/c", 1));721 try testSanitizePath("b/c", "a/b/c", 1);
692 try expectEqualStrings("c", stripComponents("a/b/c", 2));722 try testSanitizePath("c", "a/b/c", 2);
693 try expectEqualStrings("", stripComponents("a/b/c", 3));723 try testSanitizePath("", "a/b/c", 3);
694 try expectEqualStrings("", stripComponents("a/b/c", 4));724 try testSanitizePath("", "a/b/c/../../..", 0);
725 try testSanitizePathError(error.Invalid, "a/b/c", 4);
726 try testSanitizePathError(error.Invalid, "..", 0);
727 try testSanitizePathError(error.Invalid, "a/b/../../..", 0);
728 try testSanitizePathError(error.Invalid, "a/b/../..", 1);
695}729}
696730
697test PaxIterator {731test PaxIterator {