1
1
# SPDX-FileCopyrightText: OpenSSF project contributors
2
2
# SPDX-License-Identifier: MIT
3
- """ Compliant Code Example """
3
+ # SPDX-FileCopyrightText: OpenSSF project contributors
4
+ # SPDX-License-Identifier: MIT
5
+ """Compliant Code Example"""
6
+
4
7
import zipfile
5
8
from pathlib import Path
6
9
7
10
MAXSIZE = 100 * 1024 * 1024 # limit is in bytes
8
- MAXAMT = 5 # max amount of files, includes directories in the archive
11
+ MAXAMT = 1000 # max amount of files, includes directories in the archive
9
12
10
13
11
14
class ZipExtractException (Exception ):
12
15
"""Custom Exception"""
13
16
14
17
15
- def path_validation (input_path , base_path , permit_subdirs = True ):
16
- """Ensure to have only allowed path names"""
17
- test_path = (Path (base_path ) / input_path ).resolve ()
18
- if permit_subdirs :
19
- if not Path (base_path ).resolve () in test_path .resolve ().parents :
20
- raise ZipExtractException (f"Filename { test_path } not in { Path (base_path )} directory" )
21
- else :
22
- if test_path .parent != Path (base_path ).resolve ():
23
- raise ZipExtractException (f"Filename { test_path } not in { Path (base_path )} directory" )
18
+ def path_validation (filepath : Path , base_path : Path ):
19
+ """Ensure to have only allowed path names
20
+
21
+ Args:
22
+ filepath (Path): path to archive
23
+ base_path (Path): path to folder for extracting archives
24
+
25
+ Raises:
26
+ ZipExtractException: if a directory traversal is detected
27
+ """
28
+ input_path_resolved = (base_path / filepath ).resolve ()
29
+ base_path_resolved = base_path .resolve ()
30
+
31
+ if not str (input_path_resolved ).startswith (str (base_path_resolved )):
32
+ raise ZipExtractException (
33
+ f"Filename { str (input_path_resolved )} not in { str (base_path )} directory"
34
+ )
35
+
24
36
37
+ def extract_files (filepath : str , base_path : str , exist_ok : bool = True ):
38
+ """Extract archive below base_path
25
39
26
- def extract_files (file , base_path ):
27
- """Unpack zip file into base_path"""
28
- with zipfile .ZipFile (file , mode = "r" ) as archive :
29
- dirs = []
30
- # Validation:
40
+ Args:
41
+ filepath (str): path to archive
42
+ base_path (str): path to folder for extracting archives
43
+ exist_ok (bool, optional): Overwrite existing. Defaults to True.
44
+
45
+ Raises:
46
+ ZipExtractException: If there are to many files
47
+ ZipExtractException: If there are to big files
48
+ ZipExtractException: If a directory traversal is detected
49
+ """
50
+ # TODO: avoid CWE-209: Generation of Error Message Containing Sensitive Information
51
+
52
+ with zipfile .ZipFile (filepath , mode = "r" ) as archive :
53
+ # limit number of files:
31
54
if len (archive .infolist ()) > MAXAMT :
32
- raise ZipExtractException (f"Metadata check: too many files, limit is { MAXAMT } " )
33
- for zm in archive .infolist ():
34
- if zm .file_size > MAXSIZE :
35
- raise ZipExtractException (f"Metadata check: { zm .filename } is too big, limit is { MAXSIZE } " )
36
- path_validation (zm .filename , base_path )
37
- with archive .open (zm .filename , mode = 'r' ) as mte :
38
- read_data = mte .read (MAXSIZE + 1 )
39
- if len (read_data ) > MAXSIZE :
40
- raise ZipExtractException (f"File { zm .filename } bigger than { MAXSIZE } " )
55
+ raise ZipExtractException (
56
+ f"Metadata check: too many files, limit is { MAXAMT } "
57
+ )
41
58
42
- if not Path (base_path ).resolve ().exists ():
43
- Path (base_path ).resolve ().mkdir (exist_ok = True )
59
+ # validate by iterating over meta data:
60
+ for item in archive .infolist ():
61
+ # limit file size using meta data:
62
+ if item .file_size > MAXSIZE :
63
+ raise ZipExtractException (
64
+ f"Metadata check: { item .filename } is bigger than { MAXSIZE } "
65
+ )
44
66
45
- for zm in archive .infolist ():
46
- # Extraction - create directories
47
- if zm .is_dir ():
48
- dirs .append (Path (base_path ).resolve ().joinpath (zm .filename ))
67
+ path_validation (Path (item .filename ), Path (base_path ))
49
68
50
- for directory in dirs :
51
- Path .mkdir (directory )
69
+ # create target folder
70
+ Path (base_path ).mkdir (exist_ok = exist_ok )
71
+
72
+ # preparing for extraction, need to create directories first
73
+ # as they may come in random order to the files
74
+ for item in archive .infolist ():
75
+ if item .is_dir ():
76
+ xpath = Path (base_path ).joinpath (item .filename ).resolve ()
77
+ xpath .mkdir (exist_ok = exist_ok )
78
+
79
+ # start of extracting files:
80
+ for item in archive .infolist ():
81
+ if item .is_dir ():
82
+ continue
83
+ # we got a file
84
+ with archive .open (item .filename , mode = "r" ) as filehandle :
85
+ read_data = filehandle .read (MAXSIZE + 1 )
86
+ if len (read_data ) > MAXSIZE :
87
+ # meta data was lying to us, actual size is bigger:
88
+ raise ZipExtractException (
89
+ f"Reality check, { item .filename } bigger than { MAXSIZE } "
90
+ )
91
+ xpath = Path (base_path ).joinpath (filehandle .name ).resolve ()
92
+ with open (xpath , mode = "wb" ) as filehandle :
93
+ filehandle .write (read_data )
94
+ print (f"extracted successfully below { base_path } " )
52
95
53
- for zm in archive .infolist ():
54
- with archive .open (zm .filename , mode = 'r' ) as mte :
55
- xpath = Path (base_path ).joinpath (mte .name ).resolve ()
56
- print (f"Writing file { xpath } " )
57
- # Skip if directory
58
- if xpath not in dirs : # check if file is a directory
59
- with open (xpath , mode = "wb" ) as filehandle :
60
- filehandle .write (read_data )
61
96
97
+ #####################
98
+ # Trying to exploit above code example
99
+ #####################
62
100
63
- extract_files ("zip_attack_test.zip" , "ziptemp" )
101
+ extract_files ("zip_attack_test.zip" , "ziptemp" )
0 commit comments