safeuploads 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- safeuploads/__init__.py +92 -0
- safeuploads/config.py +842 -0
- safeuploads/enums.py +455 -0
- safeuploads/exceptions.py +468 -0
- safeuploads/file_validator.py +525 -0
- safeuploads/inspectors/__init__.py +10 -0
- safeuploads/inspectors/zip_inspector.py +411 -0
- safeuploads/protocols.py +51 -0
- safeuploads/validators/__init__.py +22 -0
- safeuploads/validators/base.py +43 -0
- safeuploads/validators/compression_validator.py +286 -0
- safeuploads/validators/extension_validator.py +97 -0
- safeuploads/validators/unicode_validator.py +132 -0
- safeuploads/validators/windows_validator.py +95 -0
- safeuploads-0.1.0.dist-info/METADATA +170 -0
- safeuploads-0.1.0.dist-info/RECORD +18 -0
- safeuploads-0.1.0.dist-info/WHEEL +4 -0
- safeuploads-0.1.0.dist-info/licenses/LICENSE.md +7 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
"""File security exception classes and error codes."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# ============================================================================
|
|
7
|
+
# Configuration Validation
|
|
8
|
+
# ============================================================================
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ConfigValidationError:
|
|
13
|
+
"""
|
|
14
|
+
Configuration validation issue with severity and recommendation.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
error_type: Type of the validation error.
|
|
18
|
+
message: Human-readable error message.
|
|
19
|
+
severity: Error severity level ('error', 'warning', 'info').
|
|
20
|
+
component: Component that failed validation.
|
|
21
|
+
recommendation: Optional recommendation to fix the issue.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
error_type: str
|
|
25
|
+
message: str
|
|
26
|
+
severity: str # 'error', 'warning', 'info'
|
|
27
|
+
component: str
|
|
28
|
+
recommendation: str = ""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FileSecurityConfigurationError(Exception):
|
|
32
|
+
"""
|
|
33
|
+
Configuration validation failed with aggregated errors.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
errors: List of ConfigValidationError instances.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
errors: List of validation errors that caused failure.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, errors: list[ConfigValidationError]):
|
|
43
|
+
self.errors = errors
|
|
44
|
+
error_messages = [
|
|
45
|
+
f"{error.severity.upper()}: {error.message}" for error in errors
|
|
46
|
+
]
|
|
47
|
+
super().__init__(
|
|
48
|
+
f"Configuration validation failed: {'; '.join(error_messages)}"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ============================================================================
|
|
53
|
+
# Error Codes
|
|
54
|
+
# ============================================================================
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ErrorCode:
|
|
58
|
+
"""
|
|
59
|
+
Machine-readable error codes for file validation failures.
|
|
60
|
+
|
|
61
|
+
Attributes:
|
|
62
|
+
Error codes are class-level string constants for various
|
|
63
|
+
validation failure types.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
# Filename validation errors
|
|
67
|
+
FILENAME_EMPTY = "FILENAME_EMPTY"
|
|
68
|
+
FILENAME_INVALID = "FILENAME_INVALID"
|
|
69
|
+
FILENAME_TOO_LONG = "FILENAME_TOO_LONG"
|
|
70
|
+
|
|
71
|
+
# Unicode security errors
|
|
72
|
+
UNICODE_SECURITY = "UNICODE_SECURITY"
|
|
73
|
+
UNICODE_DANGEROUS_CHARS = "UNICODE_DANGEROUS_CHARS"
|
|
74
|
+
UNICODE_NORMALIZATION_ERROR = "UNICODE_NORMALIZATION_ERROR"
|
|
75
|
+
|
|
76
|
+
# Extension validation errors
|
|
77
|
+
EXTENSION_BLOCKED = "EXTENSION_BLOCKED"
|
|
78
|
+
EXTENSION_NOT_ALLOWED = "EXTENSION_NOT_ALLOWED"
|
|
79
|
+
COMPOUND_EXTENSION_BLOCKED = "COMPOUND_EXTENSION_BLOCKED"
|
|
80
|
+
EXTENSION_MISSING = "EXTENSION_MISSING"
|
|
81
|
+
|
|
82
|
+
# Windows security errors
|
|
83
|
+
WINDOWS_RESERVED_NAME = "WINDOWS_RESERVED_NAME"
|
|
84
|
+
|
|
85
|
+
# File size errors
|
|
86
|
+
FILE_TOO_LARGE = "FILE_TOO_LARGE"
|
|
87
|
+
FILE_EMPTY = "FILE_EMPTY"
|
|
88
|
+
FILE_SIZE_UNKNOWN = "FILE_SIZE_UNKNOWN"
|
|
89
|
+
|
|
90
|
+
# MIME type errors
|
|
91
|
+
MIME_TYPE_INVALID = "MIME_TYPE_INVALID"
|
|
92
|
+
MIME_TYPE_MISMATCH = "MIME_TYPE_MISMATCH"
|
|
93
|
+
MIME_DETECTION_FAILED = "MIME_DETECTION_FAILED"
|
|
94
|
+
|
|
95
|
+
# File signature errors
|
|
96
|
+
FILE_SIGNATURE_INVALID = "FILE_SIGNATURE_INVALID"
|
|
97
|
+
FILE_SIGNATURE_MISSING = "FILE_SIGNATURE_MISSING"
|
|
98
|
+
FILE_SIGNATURE_MISMATCH = "FILE_SIGNATURE_MISMATCH"
|
|
99
|
+
|
|
100
|
+
# Compression and ZIP errors
|
|
101
|
+
ZIP_BOMB_DETECTED = "ZIP_BOMB_DETECTED"
|
|
102
|
+
ZIP_CONTENT_THREAT = "ZIP_CONTENT_THREAT"
|
|
103
|
+
COMPRESSION_RATIO_EXCEEDED = "COMPRESSION_RATIO_EXCEEDED"
|
|
104
|
+
ZIP_TOO_MANY_ENTRIES = "ZIP_TOO_MANY_ENTRIES"
|
|
105
|
+
ZIP_INVALID_STRUCTURE = "ZIP_INVALID_STRUCTURE"
|
|
106
|
+
ZIP_CORRUPT = "ZIP_CORRUPT"
|
|
107
|
+
ZIP_TOO_LARGE = "ZIP_TOO_LARGE"
|
|
108
|
+
ZIP_NESTED_ARCHIVE = "ZIP_NESTED_ARCHIVE"
|
|
109
|
+
ZIP_DIRECTORY_TRAVERSAL = "ZIP_DIRECTORY_TRAVERSAL"
|
|
110
|
+
ZIP_SYMLINK_DETECTED = "ZIP_SYMLINK_DETECTED"
|
|
111
|
+
ZIP_ABSOLUTE_PATH = "ZIP_ABSOLUTE_PATH"
|
|
112
|
+
ZIP_ANALYSIS_TIMEOUT = "ZIP_ANALYSIS_TIMEOUT"
|
|
113
|
+
|
|
114
|
+
# Processing errors
|
|
115
|
+
PROCESSING_ERROR = "PROCESSING_ERROR"
|
|
116
|
+
IO_ERROR = "IO_ERROR"
|
|
117
|
+
MEMORY_ERROR = "MEMORY_ERROR"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ============================================================================
|
|
121
|
+
# Base Exceptions
|
|
122
|
+
# ============================================================================
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class FileSecurityError(Exception):
|
|
126
|
+
"""
|
|
127
|
+
Base exception for all file security validation failures.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
message: Human-readable error description.
|
|
131
|
+
error_code: Optional machine-readable error code.
|
|
132
|
+
|
|
133
|
+
Attributes:
|
|
134
|
+
message: Human-readable error message.
|
|
135
|
+
error_code: Machine-readable error code from ErrorCode.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
def __init__(self, message: str, error_code: str | None = None):
|
|
139
|
+
self.message = message
|
|
140
|
+
self.error_code = error_code
|
|
141
|
+
super().__init__(message)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ============================================================================
|
|
145
|
+
# File Validation Exceptions
|
|
146
|
+
# ============================================================================
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class FileValidationError(FileSecurityError):
|
|
150
|
+
"""
|
|
151
|
+
File validation failed.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
message: Human-readable error description.
|
|
155
|
+
filename: Optional name of the file that failed validation.
|
|
156
|
+
error_code: Optional machine-readable error code.
|
|
157
|
+
|
|
158
|
+
Attributes:
|
|
159
|
+
filename: Name of the file that failed validation.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
def __init__(
|
|
163
|
+
self,
|
|
164
|
+
message: str,
|
|
165
|
+
filename: str | None = None,
|
|
166
|
+
error_code: str | None = None,
|
|
167
|
+
):
|
|
168
|
+
self.filename = filename
|
|
169
|
+
super().__init__(message, error_code)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ============================================================================
|
|
173
|
+
# Filename Security Exceptions
|
|
174
|
+
# ============================================================================
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class FilenameSecurityError(FileValidationError):
|
|
178
|
+
"""Filename failed security checks."""
|
|
179
|
+
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class UnicodeSecurityError(FilenameSecurityError):
|
|
184
|
+
"""
|
|
185
|
+
Dangerous Unicode characters detected in filename.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
message: Human-readable error description.
|
|
189
|
+
filename: Optional filename containing dangerous Unicode.
|
|
190
|
+
dangerous_chars: Optional list of (char, code_point, position)
|
|
191
|
+
tuples for each dangerous character found.
|
|
192
|
+
|
|
193
|
+
Attributes:
|
|
194
|
+
dangerous_chars: List of dangerous character tuples.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
def __init__(
|
|
198
|
+
self,
|
|
199
|
+
message: str,
|
|
200
|
+
filename: str | None = None,
|
|
201
|
+
dangerous_chars: list[tuple[str, int, int]] | None = None,
|
|
202
|
+
):
|
|
203
|
+
self.dangerous_chars = dangerous_chars or []
|
|
204
|
+
super().__init__(
|
|
205
|
+
message,
|
|
206
|
+
filename=filename,
|
|
207
|
+
error_code=ErrorCode.UNICODE_DANGEROUS_CHARS,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class ExtensionSecurityError(FilenameSecurityError):
|
|
212
|
+
"""
|
|
213
|
+
Dangerous file extension detected.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
message: Human-readable error description.
|
|
217
|
+
filename: Optional filename with dangerous extension.
|
|
218
|
+
extension: Optional specific extension that was blocked.
|
|
219
|
+
error_code: Optional error code (defaults to
|
|
220
|
+
EXTENSION_BLOCKED).
|
|
221
|
+
|
|
222
|
+
Attributes:
|
|
223
|
+
extension: The specific extension that was blocked.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
def __init__(
|
|
227
|
+
self,
|
|
228
|
+
message: str,
|
|
229
|
+
filename: str | None = None,
|
|
230
|
+
extension: str | None = None,
|
|
231
|
+
error_code: str | None = None,
|
|
232
|
+
):
|
|
233
|
+
self.extension = extension
|
|
234
|
+
super().__init__(
|
|
235
|
+
message,
|
|
236
|
+
filename=filename,
|
|
237
|
+
error_code=error_code or ErrorCode.EXTENSION_BLOCKED,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class WindowsReservedNameError(FilenameSecurityError):
|
|
242
|
+
"""
|
|
243
|
+
Windows reserved device name used.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
message: Human-readable error description.
|
|
247
|
+
filename: Optional filename using a reserved name.
|
|
248
|
+
reserved_name: Optional specific reserved name detected.
|
|
249
|
+
|
|
250
|
+
Attributes:
|
|
251
|
+
reserved_name: The specific reserved name that was detected.
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
def __init__(
|
|
255
|
+
self,
|
|
256
|
+
message: str,
|
|
257
|
+
filename: str | None = None,
|
|
258
|
+
reserved_name: str | None = None,
|
|
259
|
+
):
|
|
260
|
+
self.reserved_name = reserved_name
|
|
261
|
+
super().__init__(
|
|
262
|
+
message,
|
|
263
|
+
filename=filename,
|
|
264
|
+
error_code=ErrorCode.WINDOWS_RESERVED_NAME,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# ============================================================================
|
|
269
|
+
# File Content Exceptions
|
|
270
|
+
# ============================================================================
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class FileSizeError(FileValidationError):
|
|
274
|
+
"""
|
|
275
|
+
File exceeds configured size limits.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
message: Human-readable error description.
|
|
279
|
+
filename: Optional filename that exceeded size limits.
|
|
280
|
+
size: Optional actual file size in bytes.
|
|
281
|
+
max_size: Optional maximum allowed size in bytes.
|
|
282
|
+
|
|
283
|
+
Attributes:
|
|
284
|
+
size: The actual file size in bytes.
|
|
285
|
+
max_size: The maximum allowed size in bytes.
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
def __init__(
|
|
289
|
+
self,
|
|
290
|
+
message: str,
|
|
291
|
+
filename: str | None = None,
|
|
292
|
+
size: int | None = None,
|
|
293
|
+
max_size: int | None = None,
|
|
294
|
+
):
|
|
295
|
+
self.size = size
|
|
296
|
+
self.max_size = max_size
|
|
297
|
+
super().__init__(
|
|
298
|
+
message, filename=filename, error_code=ErrorCode.FILE_TOO_LARGE
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class MimeTypeError(FileValidationError):
|
|
303
|
+
"""
|
|
304
|
+
File MIME type not allowed or mismatches extension.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
message: Human-readable error description.
|
|
308
|
+
filename: Optional filename with MIME type issue.
|
|
309
|
+
detected_mime: Optional detected MIME type string.
|
|
310
|
+
allowed_mimes: Optional list of allowed MIME types.
|
|
311
|
+
error_code: Optional error code (defaults to
|
|
312
|
+
MIME_TYPE_INVALID).
|
|
313
|
+
|
|
314
|
+
Attributes:
|
|
315
|
+
detected_mime: The detected MIME type string.
|
|
316
|
+
allowed_mimes: List of allowed MIME types.
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
def __init__(
|
|
320
|
+
self,
|
|
321
|
+
message: str,
|
|
322
|
+
filename: str | None = None,
|
|
323
|
+
detected_mime: str | None = None,
|
|
324
|
+
allowed_mimes: list[str] | None = None,
|
|
325
|
+
error_code: str | None = None,
|
|
326
|
+
):
|
|
327
|
+
self.detected_mime = detected_mime
|
|
328
|
+
self.allowed_mimes = allowed_mimes or []
|
|
329
|
+
super().__init__(
|
|
330
|
+
message,
|
|
331
|
+
filename=filename,
|
|
332
|
+
error_code=error_code or ErrorCode.MIME_TYPE_INVALID,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class FileSignatureError(FileValidationError):
|
|
337
|
+
"""
|
|
338
|
+
File header signature invalid or mismatched.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
message: Human-readable error description.
|
|
342
|
+
filename: Optional filename with signature issue.
|
|
343
|
+
expected_type: Optional expected file type based on extension.
|
|
344
|
+
|
|
345
|
+
Attributes:
|
|
346
|
+
expected_type: The expected file type based on extension.
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
def __init__(
|
|
350
|
+
self,
|
|
351
|
+
message: str,
|
|
352
|
+
filename: str | None = None,
|
|
353
|
+
expected_type: str | None = None,
|
|
354
|
+
):
|
|
355
|
+
self.expected_type = expected_type
|
|
356
|
+
super().__init__(
|
|
357
|
+
message,
|
|
358
|
+
filename=filename,
|
|
359
|
+
error_code=ErrorCode.FILE_SIGNATURE_MISMATCH,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
# ============================================================================
|
|
364
|
+
# Compression and ZIP Exceptions
|
|
365
|
+
# ============================================================================
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
class CompressionSecurityError(FileValidationError):
|
|
369
|
+
"""
|
|
370
|
+
Compressed file security check failed.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
message: Human-readable error description.
|
|
374
|
+
filename: Optional filename of compressed file.
|
|
375
|
+
error_code: Optional error code (defaults to
|
|
376
|
+
COMPRESSION_GENERIC).
|
|
377
|
+
|
|
378
|
+
Attributes:
|
|
379
|
+
None beyond inherited FileValidationError attributes.
|
|
380
|
+
"""
|
|
381
|
+
|
|
382
|
+
pass
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
class ZipBombError(CompressionSecurityError):
|
|
386
|
+
"""
|
|
387
|
+
Zip archive exceeds compression ratio or uncompressed size limits.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
message: Human-readable error description.
|
|
391
|
+
filename: Optional filename of zip bomb.
|
|
392
|
+
compression_ratio: Optional actual compression ratio detected.
|
|
393
|
+
uncompressed_size: Optional total uncompressed size in bytes.
|
|
394
|
+
max_ratio: Optional maximum allowed compression ratio.
|
|
395
|
+
max_size: Optional maximum allowed uncompressed size in bytes.
|
|
396
|
+
|
|
397
|
+
Attributes:
|
|
398
|
+
compression_ratio: Actual compression ratio detected.
|
|
399
|
+
uncompressed_size: Total uncompressed size in bytes.
|
|
400
|
+
max_ratio: Maximum allowed compression ratio.
|
|
401
|
+
max_size: Maximum allowed uncompressed size in bytes.
|
|
402
|
+
"""
|
|
403
|
+
|
|
404
|
+
def __init__(
|
|
405
|
+
self,
|
|
406
|
+
message: str,
|
|
407
|
+
filename: str | None = None,
|
|
408
|
+
compression_ratio: float | None = None,
|
|
409
|
+
uncompressed_size: int | None = None,
|
|
410
|
+
max_ratio: float | None = None,
|
|
411
|
+
max_size: int | None = None,
|
|
412
|
+
):
|
|
413
|
+
self.compression_ratio = compression_ratio
|
|
414
|
+
self.uncompressed_size = uncompressed_size
|
|
415
|
+
self.max_ratio = max_ratio
|
|
416
|
+
self.max_size = max_size
|
|
417
|
+
super().__init__(
|
|
418
|
+
message,
|
|
419
|
+
filename=filename,
|
|
420
|
+
error_code=ErrorCode.ZIP_BOMB_DETECTED,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
class ZipContentError(CompressionSecurityError):
|
|
425
|
+
"""
|
|
426
|
+
Zip archive contains dangerous content or structure.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
message: Human-readable error description.
|
|
430
|
+
filename: Optional filename of problematic archive.
|
|
431
|
+
threats: Optional list of detected threat descriptions.
|
|
432
|
+
error_code: Optional error code (defaults to
|
|
433
|
+
ZIP_CONTENT_THREAT).
|
|
434
|
+
|
|
435
|
+
Attributes:
|
|
436
|
+
threats: List of detected threat descriptions.
|
|
437
|
+
"""
|
|
438
|
+
|
|
439
|
+
def __init__(
|
|
440
|
+
self,
|
|
441
|
+
message: str,
|
|
442
|
+
filename: str | None = None,
|
|
443
|
+
threats: list[str] | None = None,
|
|
444
|
+
error_code: str | None = None,
|
|
445
|
+
):
|
|
446
|
+
self.threats = threats or []
|
|
447
|
+
super().__init__(
|
|
448
|
+
message,
|
|
449
|
+
filename=filename,
|
|
450
|
+
error_code=error_code or ErrorCode.ZIP_CONTENT_THREAT,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
class FileProcessingError(FileSecurityError):
|
|
455
|
+
"""
|
|
456
|
+
Unexpected processing error during file validation.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
message: Human-readable error description.
|
|
460
|
+
original_error: Optional original exception that was caught.
|
|
461
|
+
|
|
462
|
+
Attributes:
|
|
463
|
+
original_error: The original exception that was caught.
|
|
464
|
+
"""
|
|
465
|
+
|
|
466
|
+
def __init__(self, message: str, original_error: Exception | None = None):
|
|
467
|
+
self.original_error = original_error
|
|
468
|
+
super().__init__(message, error_code=ErrorCode.PROCESSING_ERROR)
|