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