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,411 @@
|
|
|
1
|
+
"""ZIP content inspector for security threat detection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
import zipfile
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from ..enums import SuspiciousFilePattern, ZipThreatCategory
|
|
13
|
+
from ..exceptions import ZipContentError, FileProcessingError, ErrorCode
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from ..config import FileSecurityConfig
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ZipContentInspector:
|
|
23
|
+
"""
|
|
24
|
+
Inspects ZIP archive contents for security threats.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
config: File security configuration.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, config: FileSecurityConfig):
|
|
31
|
+
"""
|
|
32
|
+
Initialize ZIP inspector with configuration.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
config: File security configuration.
|
|
36
|
+
"""
|
|
37
|
+
self.config = config
|
|
38
|
+
|
|
39
|
+
def inspect_zip_content(self, file_content: bytes) -> None:
|
|
40
|
+
"""
|
|
41
|
+
Inspect ZIP archive for potential security threats.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
file_content: Raw bytes of ZIP archive.
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
ZipContentError: If security threats are detected in ZIP
|
|
48
|
+
content such as directory traversal, symlinks, nested
|
|
49
|
+
archives, or suspicious patterns.
|
|
50
|
+
FileProcessingError: If ZIP structure is invalid or
|
|
51
|
+
unexpected error occurs during inspection.
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
zip_bytes = io.BytesIO(file_content)
|
|
55
|
+
threats_found = []
|
|
56
|
+
|
|
57
|
+
# Start analysis timer
|
|
58
|
+
start_time = time.time()
|
|
59
|
+
|
|
60
|
+
with zipfile.ZipFile(zip_bytes, "r") as zip_file:
|
|
61
|
+
zip_entries = zip_file.infolist()
|
|
62
|
+
|
|
63
|
+
# Analyze each entry in the ZIP
|
|
64
|
+
for entry in zip_entries:
|
|
65
|
+
# Check for timeout
|
|
66
|
+
if (
|
|
67
|
+
time.time() - start_time
|
|
68
|
+
> self.config.limits.zip_analysis_timeout
|
|
69
|
+
):
|
|
70
|
+
logger.error(
|
|
71
|
+
"ZIP content inspection timeout",
|
|
72
|
+
extra={
|
|
73
|
+
"error_type": "zip_analysis_timeout",
|
|
74
|
+
"timeout": self.config.limits.zip_analysis_timeout,
|
|
75
|
+
},
|
|
76
|
+
)
|
|
77
|
+
raise ZipContentError(
|
|
78
|
+
message=f"ZIP content inspection timeout after {self.config.limits.zip_analysis_timeout}s",
|
|
79
|
+
threats=["Analysis timeout - potential zip bomb"],
|
|
80
|
+
error_code=ErrorCode.ZIP_ANALYSIS_TIMEOUT,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Inspect individual entry
|
|
84
|
+
entry_threats = self._inspect_zip_entry(entry, zip_file)
|
|
85
|
+
threats_found.extend(entry_threats)
|
|
86
|
+
|
|
87
|
+
# Check for ZIP structure threats
|
|
88
|
+
structure_threats = self._inspect_zip_structure(zip_entries)
|
|
89
|
+
threats_found.extend(structure_threats)
|
|
90
|
+
|
|
91
|
+
# Return results
|
|
92
|
+
if threats_found:
|
|
93
|
+
logger.warning(
|
|
94
|
+
"ZIP content threats detected",
|
|
95
|
+
extra={
|
|
96
|
+
"error_type": "zip_content_threat",
|
|
97
|
+
"threats": threats_found,
|
|
98
|
+
"threat_count": len(threats_found),
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
raise ZipContentError(
|
|
102
|
+
message=f"ZIP content threats detected: {'; '.join(threats_found)}",
|
|
103
|
+
threats=threats_found,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
logger.debug(
|
|
107
|
+
"ZIP content inspection passed: %s entries analyzed",
|
|
108
|
+
len(zip_entries),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
except ZipContentError:
|
|
112
|
+
# Re-raise our own exceptions
|
|
113
|
+
raise
|
|
114
|
+
except zipfile.BadZipFile as err:
|
|
115
|
+
logger.error("Invalid or corrupted ZIP file structure", exc_info=True)
|
|
116
|
+
raise FileProcessingError(
|
|
117
|
+
message="Invalid or corrupted ZIP file structure",
|
|
118
|
+
original_error=err,
|
|
119
|
+
) from err
|
|
120
|
+
except Exception as err:
|
|
121
|
+
logger.error(
|
|
122
|
+
"Unexpected error during ZIP content inspection",
|
|
123
|
+
exc_info=True,
|
|
124
|
+
)
|
|
125
|
+
raise FileProcessingError(
|
|
126
|
+
message=f"ZIP content inspection failed: {str(err)}",
|
|
127
|
+
original_error=err,
|
|
128
|
+
) from err
|
|
129
|
+
|
|
130
|
+
def _inspect_zip_entry(
|
|
131
|
+
self, entry: zipfile.ZipInfo, zip_file: zipfile.ZipFile
|
|
132
|
+
) -> list[str]:
|
|
133
|
+
"""
|
|
134
|
+
Inspect single ZIP entry for security threats.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
entry: ZIP entry metadata.
|
|
138
|
+
zip_file: Parent ZIP archive.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
List of threat descriptions.
|
|
142
|
+
"""
|
|
143
|
+
threats = []
|
|
144
|
+
filename = entry.filename
|
|
145
|
+
|
|
146
|
+
# 1. Check for directory traversal attacks
|
|
147
|
+
if self._has_directory_traversal(filename):
|
|
148
|
+
threats.append(f"Directory traversal attack in '{filename}'")
|
|
149
|
+
|
|
150
|
+
# 2. Check for absolute paths
|
|
151
|
+
if not self.config.limits.allow_absolute_paths and self._has_absolute_path(
|
|
152
|
+
filename
|
|
153
|
+
):
|
|
154
|
+
threats.append(f"Absolute path detected in '{filename}'")
|
|
155
|
+
|
|
156
|
+
# 3. Check for symbolic links
|
|
157
|
+
if not self.config.limits.allow_symlinks and self._is_symlink(entry):
|
|
158
|
+
threats.append(f"Symbolic link detected: '{filename}'")
|
|
159
|
+
|
|
160
|
+
# 4. Check filename length limits
|
|
161
|
+
if len(os.path.basename(filename)) > self.config.limits.max_filename_length:
|
|
162
|
+
threats.append(
|
|
163
|
+
f"Filename too long: '{filename}' ({len(os.path.basename(filename))} chars)"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# 5. Check path length limits
|
|
167
|
+
if len(filename) > self.config.limits.max_path_length:
|
|
168
|
+
threats.append(f"Path too long: '{filename}' ({len(filename)} chars)")
|
|
169
|
+
|
|
170
|
+
# 6. Check for suspicious filename patterns
|
|
171
|
+
suspicious_patterns = self._check_suspicious_patterns(filename)
|
|
172
|
+
threats.extend(suspicious_patterns)
|
|
173
|
+
|
|
174
|
+
# 7. Check for nested archives
|
|
175
|
+
if not self.config.limits.allow_nested_archives and self._is_nested_archive(
|
|
176
|
+
filename
|
|
177
|
+
):
|
|
178
|
+
threats.append(f"Nested archive detected: '{filename}'")
|
|
179
|
+
|
|
180
|
+
# 8. Check file content if enabled and entry is small enough
|
|
181
|
+
if (
|
|
182
|
+
self.config.limits.scan_zip_content
|
|
183
|
+
and not entry.is_dir()
|
|
184
|
+
and entry.file_size < 1024 * 1024
|
|
185
|
+
): # 1MB limit for content scan
|
|
186
|
+
content_threats = self._inspect_entry_content(entry, zip_file)
|
|
187
|
+
threats.extend(content_threats)
|
|
188
|
+
|
|
189
|
+
return threats
|
|
190
|
+
|
|
191
|
+
def _inspect_zip_structure(self, entries: list[zipfile.ZipInfo]) -> list[str]:
|
|
192
|
+
"""
|
|
193
|
+
Inspect ZIP structure for anomalies.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
entries: All ZIP entries to analyze.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
List of structural threat descriptions.
|
|
200
|
+
"""
|
|
201
|
+
threats = []
|
|
202
|
+
|
|
203
|
+
# Check directory depth
|
|
204
|
+
max_depth = 0
|
|
205
|
+
for entry in entries:
|
|
206
|
+
depth = entry.filename.count("/") + entry.filename.count("\\")
|
|
207
|
+
max_depth = max(max_depth, depth)
|
|
208
|
+
|
|
209
|
+
if max_depth > self.config.limits.max_zip_depth:
|
|
210
|
+
threats.append(
|
|
211
|
+
f"Excessive directory depth: {max_depth} (max: {self.config.limits.max_zip_depth})"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Check for suspicious file distribution
|
|
215
|
+
file_types = {}
|
|
216
|
+
for entry in entries:
|
|
217
|
+
if not entry.is_dir():
|
|
218
|
+
ext = os.path.splitext(entry.filename)[1].lower()
|
|
219
|
+
file_types[ext] = file_types.get(ext, 0) + 1
|
|
220
|
+
|
|
221
|
+
# Check for excessive number of same-type files (potential spam/bomb)
|
|
222
|
+
for ext, count in file_types.items():
|
|
223
|
+
if count > 1000: # More than 1000 files of same type
|
|
224
|
+
threats.append(f"Excessive number of {ext} files: {count}")
|
|
225
|
+
|
|
226
|
+
return threats
|
|
227
|
+
|
|
228
|
+
def _has_directory_traversal(self, filename: str) -> bool:
|
|
229
|
+
"""
|
|
230
|
+
Check for directory traversal indicators.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
filename: Filename to check.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
True if traversal detected.
|
|
237
|
+
"""
|
|
238
|
+
filename_lower = filename.lower()
|
|
239
|
+
|
|
240
|
+
for category in SuspiciousFilePattern:
|
|
241
|
+
if category == SuspiciousFilePattern.DIRECTORY_TRAVERSAL:
|
|
242
|
+
for pattern in category.value:
|
|
243
|
+
if pattern.lower() in filename_lower:
|
|
244
|
+
return True
|
|
245
|
+
|
|
246
|
+
# Additional checks for normalized paths
|
|
247
|
+
normalized = os.path.normpath(filename)
|
|
248
|
+
if normalized.startswith("..") or "/.." in normalized or "\\.." in normalized:
|
|
249
|
+
return True
|
|
250
|
+
|
|
251
|
+
return False
|
|
252
|
+
|
|
253
|
+
def _has_absolute_path(self, filename: str) -> bool:
|
|
254
|
+
"""
|
|
255
|
+
Check if filename is an absolute path.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
filename: Path to check.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
True if absolute path detected.
|
|
262
|
+
"""
|
|
263
|
+
return (
|
|
264
|
+
filename.startswith("/") # Unix absolute path
|
|
265
|
+
or filename.startswith("\\") # Windows UNC path
|
|
266
|
+
or (len(filename) > 1 and filename[1] == ":") # Windows drive path
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def _is_symlink(self, entry: zipfile.ZipInfo) -> bool:
|
|
270
|
+
"""
|
|
271
|
+
Check if entry is a symbolic link.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
entry: ZIP entry to check.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
True if entry is a symlink.
|
|
278
|
+
"""
|
|
279
|
+
# Check if entry has symlink attributes
|
|
280
|
+
return (entry.external_attr >> 16) & 0o120000 == 0o120000
|
|
281
|
+
|
|
282
|
+
def _check_suspicious_patterns(self, filename: str) -> list[str]:
|
|
283
|
+
"""
|
|
284
|
+
Check filename for suspicious patterns.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
filename: Filename to check.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
List of pattern warnings.
|
|
291
|
+
"""
|
|
292
|
+
threats = []
|
|
293
|
+
filename_lower = filename.lower()
|
|
294
|
+
basename = os.path.basename(filename_lower)
|
|
295
|
+
|
|
296
|
+
# Check suspicious names
|
|
297
|
+
for pattern in SuspiciousFilePattern.SUSPICIOUS_NAMES.value:
|
|
298
|
+
if basename == pattern.lower():
|
|
299
|
+
threats.append(f"Suspicious filename pattern: '{filename}'")
|
|
300
|
+
break
|
|
301
|
+
|
|
302
|
+
# Check suspicious path components
|
|
303
|
+
for pattern in SuspiciousFilePattern.SUSPICIOUS_PATHS.value:
|
|
304
|
+
if pattern.lower() in filename_lower:
|
|
305
|
+
threats.append(
|
|
306
|
+
f"Suspicious path component: '{filename}' contains '{pattern}'"
|
|
307
|
+
)
|
|
308
|
+
break
|
|
309
|
+
|
|
310
|
+
return threats
|
|
311
|
+
|
|
312
|
+
def _is_nested_archive(self, filename: str) -> bool:
|
|
313
|
+
"""
|
|
314
|
+
Check if filename represents a nested archive.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
filename: Filename to check.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
True if nested archive detected.
|
|
321
|
+
"""
|
|
322
|
+
ext = os.path.splitext(filename)[1].lower()
|
|
323
|
+
|
|
324
|
+
for category in ZipThreatCategory:
|
|
325
|
+
if category == ZipThreatCategory.NESTED_ARCHIVES:
|
|
326
|
+
return ext in category.value
|
|
327
|
+
|
|
328
|
+
return False
|
|
329
|
+
|
|
330
|
+
def _inspect_entry_content(
|
|
331
|
+
self, entry: zipfile.ZipInfo, zip_file: zipfile.ZipFile
|
|
332
|
+
) -> list[str]:
|
|
333
|
+
"""
|
|
334
|
+
Inspect ZIP entry content for malicious signatures.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
entry: ZIP entry to inspect.
|
|
338
|
+
zip_file: Parent ZIP archive.
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
List of content threat descriptions.
|
|
342
|
+
"""
|
|
343
|
+
threats = []
|
|
344
|
+
|
|
345
|
+
try:
|
|
346
|
+
# Read first few bytes to check for executable signatures
|
|
347
|
+
with zip_file.open(entry, "r") as file:
|
|
348
|
+
content_sample = file.read(512) # Read first 512 bytes
|
|
349
|
+
|
|
350
|
+
# Check for executable signatures
|
|
351
|
+
for signature in SuspiciousFilePattern.EXECUTABLE_SIGNATURES.value:
|
|
352
|
+
if content_sample.startswith(signature):
|
|
353
|
+
threats.append(
|
|
354
|
+
f"Executable content detected in '{entry.filename}'"
|
|
355
|
+
)
|
|
356
|
+
break
|
|
357
|
+
|
|
358
|
+
# Check for script content patterns
|
|
359
|
+
if self._contains_script_patterns(content_sample, entry.filename):
|
|
360
|
+
threats.append(f"Script content detected in '{entry.filename}'")
|
|
361
|
+
|
|
362
|
+
except Exception as err:
|
|
363
|
+
logger.warning(
|
|
364
|
+
"Could not inspect content of '%s': %s",
|
|
365
|
+
entry.filename,
|
|
366
|
+
err,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
return threats
|
|
370
|
+
|
|
371
|
+
def _contains_script_patterns(self, content: bytes, filename: str) -> bool:
|
|
372
|
+
"""
|
|
373
|
+
Check content for malicious script patterns.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
content: Raw bytes to inspect.
|
|
377
|
+
filename: Filename for context.
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
True if script patterns found.
|
|
381
|
+
"""
|
|
382
|
+
try:
|
|
383
|
+
# Try to decode as text
|
|
384
|
+
text_content = content.decode("utf-8", errors="ignore").lower()
|
|
385
|
+
|
|
386
|
+
# Check for common script patterns
|
|
387
|
+
script_patterns = [
|
|
388
|
+
"#!/bin/",
|
|
389
|
+
"#!/usr/bin/",
|
|
390
|
+
"powershell",
|
|
391
|
+
"cmd.exe",
|
|
392
|
+
"eval(",
|
|
393
|
+
"exec(",
|
|
394
|
+
"system(",
|
|
395
|
+
"shell_exec(",
|
|
396
|
+
"<script",
|
|
397
|
+
"<?php",
|
|
398
|
+
"<%",
|
|
399
|
+
"import os",
|
|
400
|
+
"import subprocess",
|
|
401
|
+
]
|
|
402
|
+
|
|
403
|
+
for pattern in script_patterns:
|
|
404
|
+
if pattern in text_content:
|
|
405
|
+
return True
|
|
406
|
+
|
|
407
|
+
except Exception:
|
|
408
|
+
# If we can't decode as text, it's probably binary
|
|
409
|
+
pass
|
|
410
|
+
|
|
411
|
+
return False
|
safeuploads/protocols.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Framework-agnostic protocols for file upload handling.
|
|
3
|
+
|
|
4
|
+
This module defines protocols that allow safeuploads to work with any
|
|
5
|
+
web framework's file upload implementation without depending on specific
|
|
6
|
+
framework packages.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Protocol, runtime_checkable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@runtime_checkable
|
|
13
|
+
class UploadFileProtocol(Protocol):
|
|
14
|
+
"""
|
|
15
|
+
Protocol for file upload objects from any web framework.
|
|
16
|
+
|
|
17
|
+
This protocol defines the minimal interface required for file
|
|
18
|
+
validation. Any object with these attributes and methods can be
|
|
19
|
+
validated, regardless of the web framework being used.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
filename: Original filename from the client.
|
|
23
|
+
size: Size of the uploaded file in bytes.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
filename: str | None
|
|
27
|
+
size: int | None
|
|
28
|
+
|
|
29
|
+
async def read(self, size: int = -1) -> bytes:
|
|
30
|
+
"""
|
|
31
|
+
Read bytes from the uploaded file.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
size: Number of bytes to read. -1 reads entire file.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Bytes read from the file.
|
|
38
|
+
"""
|
|
39
|
+
...
|
|
40
|
+
|
|
41
|
+
async def seek(self, offset: int) -> int:
|
|
42
|
+
"""
|
|
43
|
+
Move file pointer to specified position.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
offset: Position to move to in bytes.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
New position in the file.
|
|
50
|
+
"""
|
|
51
|
+
...
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Security validation modules for uploaded files.
|
|
3
|
+
|
|
4
|
+
This package provides validators that check filenames and file
|
|
5
|
+
properties for potential security threats including Unicode attacks,
|
|
6
|
+
invalid extensions, Windows-specific vulnerabilities, and compression
|
|
7
|
+
bombs.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .base import BaseValidator
|
|
11
|
+
from .unicode_validator import UnicodeSecurityValidator
|
|
12
|
+
from .extension_validator import ExtensionSecurityValidator
|
|
13
|
+
from .windows_validator import WindowsSecurityValidator
|
|
14
|
+
from .compression_validator import CompressionSecurityValidator
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"BaseValidator",
|
|
18
|
+
"UnicodeSecurityValidator",
|
|
19
|
+
"ExtensionSecurityValidator",
|
|
20
|
+
"WindowsSecurityValidator",
|
|
21
|
+
"CompressionSecurityValidator",
|
|
22
|
+
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base validator interface for file security checks.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from ..config import FileSecurityConfig
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseValidator(ABC):
|
|
15
|
+
"""
|
|
16
|
+
Abstract base class for file security validators.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
config: File security configuration parameters.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, config: FileSecurityConfig):
|
|
23
|
+
"""
|
|
24
|
+
Initialize validator with configuration.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
config: File security settings to apply.
|
|
28
|
+
"""
|
|
29
|
+
self.config = config
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def validate(self, *args, **kwargs) -> Any:
|
|
33
|
+
"""
|
|
34
|
+
Validate data using subclass-specific logic.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
*args: Positional arguments for concrete validator.
|
|
38
|
+
**kwargs: Keyword arguments for concrete validator.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Validated result defined by subclass.
|
|
42
|
+
"""
|
|
43
|
+
pass
|