Skip to content

Commit 3ab117d

Browse files
authored
Merge pull request #206 from mtelvers/fix-dot-in-dst-tar-paths
Also strip "./" segments from destination tar paths
2 parents a4329f0 + 56361d2 commit 3ab117d

3 files changed

Lines changed: 31 additions & 6 deletions

File tree

lib/tar_transfer.ml

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,15 +121,20 @@ and send_dir ~src_dir ~dst ~to_untar ~user items =
121121
copy_dir ~src_dir ~src ~dst ~items ~to_untar ~user
122122
)
123123

124-
let remove_leading_slashes s =
124+
let normalize_path s =
125125
(* Strip Windows drive letter prefix (e.g. "C:/") *)
126126
let s =
127127
if String.length s >= 2 && Char.uppercase_ascii s.[0] >= 'A' &&
128128
Char.uppercase_ascii s.[0] <= 'Z' && s.[1] = ':' then
129129
String.sub s 2 (String.length s - 2)
130130
else s
131131
in
132-
Astring.String.drop ~sat:((=) '/') s
132+
(* Drop leading slashes and any "." or empty path components. openSUSE's
133+
backport of CVE-2025-45582 to tar 1.34 walks paths with openat() and
134+
fails on "./" segments in the header path. *)
135+
String.split_on_char '/' s
136+
|> List.filter (fun p -> p <> "" && p <> ".")
137+
|> String.concat "/"
133138

134139
let ensure_dir_entries ~to_untar ~user path =
135140
(* Emit tar directory entries for each component of path so that
@@ -150,13 +155,13 @@ let ensure_dir_entries ~to_untar ~user path =
150155
loop "" parts
151156

152157
let send_files ~src_dir ~src_manifest ~dst_dir ~user ~to_untar =
153-
let dst = remove_leading_slashes dst_dir in
158+
let dst = normalize_path dst_dir in
154159
ensure_dir_entries ~to_untar ~user dst >>= fun () ->
155160
send_dir ~src_dir ~dst ~to_untar ~user src_manifest >>= fun () ->
156161
Tar_lwt_unix.write_end to_untar
157162

158163
let send_file ~src_dir ~src_manifest ~dst ~user ~to_untar =
159-
let dst = remove_leading_slashes dst in
164+
let dst = normalize_path dst in
160165
begin
161166
match src_manifest with
162167
| `File (path, _) ->
@@ -214,7 +219,7 @@ let rec map_transform ~dst transformations = function
214219
List.iter (map_transform ~dst transformations) items
215220

216221
and transform_files ~from_tar ~src_manifest ~dst_dir ~user ~to_untar =
217-
let dst = remove_leading_slashes dst_dir in
222+
let dst = normalize_path dst_dir in
218223
let transformations = Hashtbl.create ~random:true 64 in
219224
List.iter (map_transform ~dst transformations) src_manifest;
220225
let fname file_name =
@@ -225,7 +230,7 @@ and transform_files ~from_tar ~src_manifest ~dst_dir ~user ~to_untar =
225230
Tar_lwt_unix.Archive.transform ~level (transform ~user fname) from_tar to_untar
226231

227232
let transform_file ~from_tar ~src_manifest ~dst ~user ~to_untar =
228-
let dst = remove_leading_slashes dst in
233+
let dst = normalize_path dst in
229234
let transformations = Hashtbl.create ~random:true 1 in
230235
let map_transform = function
231236
| `File (src, _) -> Hashtbl.add transformations src dst

lib/tar_transfer.mli

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
val normalize_path : string -> string
2+
(** [normalize_path p] strips any Windows drive-letter prefix, leading slashes,
3+
and "." or empty path components. Used to clean destination paths before
4+
they are written into tar headers, so that "./" segments do not break
5+
openSUSE's backport of CVE-2025-45582 into tar 1.34. *)
6+
17
val send_files :
28
src_dir:string ->
39
src_manifest:Manifest.t list ->

test/test.ml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,19 @@ let test_tar_long_filename _switch () =
456456
(* Maximum path length on Windows is 260 characters. *)
457457
do_test (260 - 1 (* NUL *) - String.length {|C:\cygwin64\tmp\build_123456_dune\test-copy-src-123456\|})
458458

459+
let test_tar_normalize_path () =
460+
let check = Alcotest.(check string) in
461+
check "passthrough" "home/opam/repo" (Tar_transfer.normalize_path "home/opam/repo");
462+
check "leading slash" "home/opam/repo" (Tar_transfer.normalize_path "/home/opam/repo");
463+
check "trailing slash" "home/opam/repo" (Tar_transfer.normalize_path "/home/opam/repo/");
464+
check "dot segment in middle" "src/base.opam" (Tar_transfer.normalize_path "/src/./base.opam");
465+
check "dot-slash at end" "src" (Tar_transfer.normalize_path "/src/./");
466+
check "dot-slash at start" "foo/bar" (Tar_transfer.normalize_path "./foo/bar");
467+
check "only dot" "" (Tar_transfer.normalize_path ".");
468+
check "only dot-slash" "" (Tar_transfer.normalize_path "./");
469+
check "windows drive" "foo/bar" (Tar_transfer.normalize_path "C:/foo/bar");
470+
check "windows drive with dot" "foo/bar" (Tar_transfer.normalize_path "C:/./foo/bar")
471+
459472
let sexp = Alcotest.of_pp Sexplib.Sexp.pp_hum
460473

461474
let remove_line_indents = function
@@ -857,6 +870,7 @@ let () =
857870
];
858871
"tar_transfer", [
859872
test_case "Long filename" `Quick test_tar_long_filename;
873+
test_case_sync "Normalize path" `Quick test_tar_normalize_path;
860874
];
861875
"manifest", [
862876
test_case "Copy using manifest.bash" `Quick test_copy_bash;

0 commit comments

Comments
 (0)