1
+ use std:: io:: Write ;
2
+ use zip:: write:: SimpleFileOptions ;
3
+ use zip:: { ZipArchive , ZipWriter } ;
4
+
5
+ /// Create a ZIP file with entries that have absolute paths (starting with /)
6
+ /// This simulates the problematic ZIP file mentioned in the bug report
7
+ fn create_zip_with_absolute_paths ( ) -> Vec < u8 > {
8
+ let buf = Vec :: new ( ) ;
9
+ let mut writer = ZipWriter :: new ( std:: io:: Cursor :: new ( buf) ) ;
10
+ let options = SimpleFileOptions :: default ( ) ;
11
+
12
+ // Create entries with absolute paths - this should cause "Invalid file path" error
13
+ writer. add_directory ( "/_/" , options) . unwrap ( ) ;
14
+ writer. start_file ( "/_/file1.txt" , options) . unwrap ( ) ;
15
+ writer. write_all ( b"File 1 content" ) . unwrap ( ) ;
16
+ writer. start_file ( "/_/subdir/file2.txt" , options) . unwrap ( ) ;
17
+ writer. write_all ( b"File 2 content" ) . unwrap ( ) ;
18
+
19
+ writer. finish ( ) . unwrap ( ) . into_inner ( )
20
+ }
21
+
22
+ /// Create a ZIP file with entries that have Windows-style absolute paths
23
+ fn create_zip_with_windows_absolute_paths ( ) -> Vec < u8 > {
24
+ let buf = Vec :: new ( ) ;
25
+ let mut writer = ZipWriter :: new ( std:: io:: Cursor :: new ( buf) ) ;
26
+ let options = SimpleFileOptions :: default ( ) ;
27
+
28
+ // Create entries with Windows absolute paths
29
+ writer. add_directory ( "C:\\ temp\\ " , options) . unwrap ( ) ;
30
+ writer. start_file ( "C:\\ temp\\ file1.txt" , options) . unwrap ( ) ;
31
+ writer. write_all ( b"File 1 content" ) . unwrap ( ) ;
32
+
33
+ writer. finish ( ) . unwrap ( ) . into_inner ( )
34
+ }
35
+
36
+ /// Create a ZIP file that more closely simulates the soldeer registry issue
37
+ /// with an underscore directory at the root with absolute path
38
+ fn create_zip_like_soldeer_issue ( ) -> Vec < u8 > {
39
+ let buf = Vec :: new ( ) ;
40
+ let mut writer = ZipWriter :: new ( std:: io:: Cursor :: new ( buf) ) ;
41
+ let options = SimpleFileOptions :: default ( ) ;
42
+
43
+ // Simulate the soldeer registry structure with absolute paths
44
+ writer. add_directory ( "/_/" , options) . unwrap ( ) ;
45
+ writer. add_directory ( "/_/forge-std/" , options) . unwrap ( ) ;
46
+ writer. start_file ( "/_/forge-std/src/Test.sol" , options) . unwrap ( ) ;
47
+ writer. write_all ( b"// SPDX-License-Identifier: MIT\n pragma solidity ^0.8.0;\n " ) . unwrap ( ) ;
48
+ writer. start_file ( "/_/forge-std/lib/ds-test/src/test.sol" , options) . unwrap ( ) ;
49
+ writer. write_all ( b"// Test contract\n " ) . unwrap ( ) ;
50
+
51
+ writer. finish ( ) . unwrap ( ) . into_inner ( )
52
+ }
53
+
54
+ #[ test]
55
+ fn test_extract_zip_with_absolute_paths ( ) {
56
+ let zip_data = create_zip_with_absolute_paths ( ) ;
57
+ let mut archive = ZipArchive :: new ( std:: io:: Cursor :: new ( zip_data) ) . unwrap ( ) ;
58
+
59
+ // After fix: should extract successfully, stripping the leading /
60
+ let temp_dir = tempfile:: TempDir :: new ( ) . unwrap ( ) ;
61
+ archive. extract ( temp_dir. path ( ) ) . unwrap ( ) ;
62
+
63
+ // Files should be extracted with the absolute path prefix stripped
64
+ assert ! ( temp_dir. path( ) . join( "_" ) . exists( ) ) ;
65
+ assert ! ( temp_dir. path( ) . join( "_/file1.txt" ) . exists( ) ) ;
66
+ assert ! ( temp_dir. path( ) . join( "_/subdir/file2.txt" ) . exists( ) ) ;
67
+
68
+ // Verify file contents
69
+ let content = std:: fs:: read_to_string ( temp_dir. path ( ) . join ( "_/file1.txt" ) ) . unwrap ( ) ;
70
+ assert_eq ! ( content, "File 1 content" ) ;
71
+ }
72
+
73
+ #[ test]
74
+ fn test_extract_zip_with_windows_absolute_paths ( ) {
75
+ let zip_data = create_zip_with_windows_absolute_paths ( ) ;
76
+ let mut archive = ZipArchive :: new ( std:: io:: Cursor :: new ( zip_data) ) . unwrap ( ) ;
77
+
78
+ // After fix: should extract successfully, stripping the C:\ prefix
79
+ let temp_dir = tempfile:: TempDir :: new ( ) . unwrap ( ) ;
80
+ archive. extract ( temp_dir. path ( ) ) . unwrap ( ) ;
81
+
82
+ // Files should be extracted with the Windows absolute path prefix stripped
83
+ assert ! ( temp_dir. path( ) . join( "temp" ) . exists( ) ) ;
84
+ assert ! ( temp_dir. path( ) . join( "temp/file1.txt" ) . exists( ) ) ;
85
+
86
+ // Verify file contents
87
+ let content = std:: fs:: read_to_string ( temp_dir. path ( ) . join ( "temp/file1.txt" ) ) . unwrap ( ) ;
88
+ assert_eq ! ( content, "File 1 content" ) ;
89
+ }
90
+
91
+ #[ test]
92
+ fn test_extract_soldeer_like_zip ( ) {
93
+ let zip_data = create_zip_like_soldeer_issue ( ) ;
94
+ let mut archive = ZipArchive :: new ( std:: io:: Cursor :: new ( zip_data) ) . unwrap ( ) ;
95
+
96
+ // This should now work without "Invalid file path" error
97
+ let temp_dir = tempfile:: TempDir :: new ( ) . unwrap ( ) ;
98
+ archive. extract ( temp_dir. path ( ) ) . unwrap ( ) ;
99
+
100
+ // Verify the structure is extracted correctly with absolute prefix stripped
101
+ assert ! ( temp_dir. path( ) . join( "_" ) . exists( ) ) ;
102
+ assert ! ( temp_dir. path( ) . join( "_/forge-std" ) . exists( ) ) ;
103
+ assert ! ( temp_dir. path( ) . join( "_/forge-std/src/Test.sol" ) . exists( ) ) ;
104
+ assert ! ( temp_dir. path( ) . join( "_/forge-std/lib/ds-test/src/test.sol" ) . exists( ) ) ;
105
+
106
+ // Verify file contents
107
+ let content = std:: fs:: read_to_string ( temp_dir. path ( ) . join ( "_/forge-std/src/Test.sol" ) ) . unwrap ( ) ;
108
+ assert ! ( content. contains( "SPDX-License-Identifier" ) ) ;
109
+ }
110
+
111
+ #[ test]
112
+ fn test_individual_file_access_with_absolute_paths ( ) {
113
+ let zip_data = create_zip_with_absolute_paths ( ) ;
114
+ let mut archive = ZipArchive :: new ( std:: io:: Cursor :: new ( zip_data) ) . unwrap ( ) ;
115
+
116
+ // Test accessing individual files
117
+ for i in 0 ..archive. len ( ) {
118
+ let file = archive. by_index ( i) . unwrap ( ) ;
119
+ println ! ( "File name: {}" , file. name( ) ) ;
120
+
121
+ // After our fix, enclosed_name should return a safe relative path
122
+ let enclosed_name = file. enclosed_name ( ) ;
123
+ println ! ( "Enclosed name: {:?}" , enclosed_name) ;
124
+
125
+ // Should now return Some with the absolute prefix stripped
126
+ assert ! ( enclosed_name. is_some( ) ) ;
127
+ let path = enclosed_name. unwrap ( ) ;
128
+
129
+ // Verify the path doesn't start with / or contain absolute components
130
+ assert ! ( !path. is_absolute( ) ) ;
131
+ assert ! ( !path. to_string_lossy( ) . starts_with( '/' ) ) ;
132
+ }
133
+ }
134
+
135
+ #[ test]
136
+ fn test_security_still_prevents_directory_traversal ( ) {
137
+ let buf = Vec :: new ( ) ;
138
+ let mut writer = ZipWriter :: new ( std:: io:: Cursor :: new ( buf) ) ;
139
+ let options = SimpleFileOptions :: default ( ) ;
140
+
141
+ // Create a ZIP with directory traversal attempts
142
+ writer. start_file ( "../../../etc/passwd" , options) . unwrap ( ) ;
143
+ writer. write_all ( b"malicious content" ) . unwrap ( ) ;
144
+ writer. start_file ( "foo/../../../etc/shadow" , options) . unwrap ( ) ;
145
+ writer. write_all ( b"more malicious content" ) . unwrap ( ) ;
146
+
147
+ let zip_data = writer. finish ( ) . unwrap ( ) . into_inner ( ) ;
148
+ let mut archive = ZipArchive :: new ( std:: io:: Cursor :: new ( zip_data) ) . unwrap ( ) ;
149
+
150
+ // These should still fail due to directory traversal protection
151
+ for i in 0 ..archive. len ( ) {
152
+ let file = archive. by_index ( i) . unwrap ( ) ;
153
+ let enclosed_name = file. enclosed_name ( ) ;
154
+
155
+ // Directory traversal attempts should still return None
156
+ assert ! ( enclosed_name. is_none( ) , "Directory traversal should still be blocked for: {}" , file. name( ) ) ;
157
+ }
158
+ }
0 commit comments