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