safeuploads 2.0.0__tar.gz → 2.1.0__tar.gz

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.
Files changed (106) hide show
  1. {safeuploads-2.0.0 → safeuploads-2.1.0}/.agents/skills/safeuploads-library/SKILL.md +1 -1
  2. {safeuploads-2.0.0 → safeuploads-2.1.0}/AGENTS.md +1 -1
  3. {safeuploads-2.0.0 → safeuploads-2.1.0}/CHANGELOG.md +16 -0
  4. {safeuploads-2.0.0 → safeuploads-2.1.0}/PKG-INFO +15 -2
  5. {safeuploads-2.0.0 → safeuploads-2.1.0}/README.md +14 -1
  6. {safeuploads-2.0.0 → safeuploads-2.1.0}/docs/index.md +14 -1
  7. {safeuploads-2.0.0 → safeuploads-2.1.0}/docs/security/architecture.md +4 -2
  8. {safeuploads-2.0.0 → safeuploads-2.1.0}/pyproject.toml +2 -2
  9. {safeuploads-2.0.0 → safeuploads-2.1.0}/safeuploads/file_validator.py +631 -209
  10. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/test_file_validator.py +34 -0
  11. safeuploads-2.1.0/tests/test_sync_validation.py +817 -0
  12. safeuploads-2.1.0/uv.lock +1452 -0
  13. safeuploads-2.0.0/uv.lock +0 -1339
  14. {safeuploads-2.0.0 → safeuploads-2.1.0}/.agents/skills/conventional-commits/SKILL.md +0 -0
  15. {safeuploads-2.0.0 → safeuploads-2.1.0}/.agents/skills/conventional-commits/references/pull-requests.md +0 -0
  16. {safeuploads-2.0.0 → safeuploads-2.1.0}/.agents/skills/debugging/SKILL.md +0 -0
  17. {safeuploads-2.0.0 → safeuploads-2.1.0}/.agents/skills/safeuploads-library/references/code-standards.md +0 -0
  18. {safeuploads-2.0.0 → safeuploads-2.1.0}/.agents/skills/safeuploads-library/references/security.md +0 -0
  19. {safeuploads-2.0.0 → safeuploads-2.1.0}/.agents/skills/safeuploads-library/references/testing.md +0 -0
  20. {safeuploads-2.0.0 → safeuploads-2.1.0}/.agents/skills/skill-authoring/SKILL.md +0 -0
  21. {safeuploads-2.0.0 → safeuploads-2.1.0}/.github/CODEOWNERS +0 -0
  22. {safeuploads-2.0.0 → safeuploads-2.1.0}/.github/FUNDING.yml +0 -0
  23. {safeuploads-2.0.0 → safeuploads-2.1.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  24. {safeuploads-2.0.0 → safeuploads-2.1.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  25. {safeuploads-2.0.0 → safeuploads-2.1.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  26. {safeuploads-2.0.0 → safeuploads-2.1.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  27. {safeuploads-2.0.0 → safeuploads-2.1.0}/.github/copilot-instructions.md +0 -0
  28. {safeuploads-2.0.0 → safeuploads-2.1.0}/.github/dependabot.yml +0 -0
  29. {safeuploads-2.0.0 → safeuploads-2.1.0}/.github/scripts/check_conventional_commits.py +0 -0
  30. {safeuploads-2.0.0 → safeuploads-2.1.0}/.github/scripts/smoke_import.py +0 -0
  31. {safeuploads-2.0.0 → safeuploads-2.1.0}/.github/workflows/audit-safeuploads.yml +0 -0
  32. {safeuploads-2.0.0 → safeuploads-2.1.0}/.github/workflows/conventional-commits.yml +0 -0
  33. {safeuploads-2.0.0 → safeuploads-2.1.0}/.github/workflows/docs.yml +0 -0
  34. {safeuploads-2.0.0 → safeuploads-2.1.0}/.github/workflows/fuzz.yml +0 -0
  35. {safeuploads-2.0.0 → safeuploads-2.1.0}/.github/workflows/lint-and-test.yml +0 -0
  36. {safeuploads-2.0.0 → safeuploads-2.1.0}/.github/workflows/mirror.yml +0 -0
  37. {safeuploads-2.0.0 → safeuploads-2.1.0}/.github/workflows/publish-safeuploads.yml +0 -0
  38. {safeuploads-2.0.0 → safeuploads-2.1.0}/.github/workflows/test-matrix-safeuploads.yml +0 -0
  39. {safeuploads-2.0.0 → safeuploads-2.1.0}/.gitignore +0 -0
  40. {safeuploads-2.0.0 → safeuploads-2.1.0}/CODE_OF_CONDUCT.md +0 -0
  41. {safeuploads-2.0.0 → safeuploads-2.1.0}/CONTRIBUTING.md +0 -0
  42. {safeuploads-2.0.0 → safeuploads-2.1.0}/LICENSE.md +0 -0
  43. {safeuploads-2.0.0 → safeuploads-2.1.0}/SECURITY.md +0 -0
  44. {safeuploads-2.0.0 → safeuploads-2.1.0}/docs/api.md +0 -0
  45. {safeuploads-2.0.0 → safeuploads-2.1.0}/docs/rate-limiting.md +0 -0
  46. {safeuploads-2.0.0 → safeuploads-2.1.0}/docs/security/integration-checklist.md +0 -0
  47. {safeuploads-2.0.0 → safeuploads-2.1.0}/docs/security/threat-model.md +0 -0
  48. {safeuploads-2.0.0 → safeuploads-2.1.0}/examples/README.md +0 -0
  49. {safeuploads-2.0.0 → safeuploads-2.1.0}/examples/fastapi_example.py +0 -0
  50. {safeuploads-2.0.0 → safeuploads-2.1.0}/mkdocs.yml +0 -0
  51. {safeuploads-2.0.0 → safeuploads-2.1.0}/safeuploads/__init__.py +0 -0
  52. {safeuploads-2.0.0 → safeuploads-2.1.0}/safeuploads/audit.py +0 -0
  53. {safeuploads-2.0.0 → safeuploads-2.1.0}/safeuploads/config.py +0 -0
  54. {safeuploads-2.0.0 → safeuploads-2.1.0}/safeuploads/enums.py +0 -0
  55. {safeuploads-2.0.0 → safeuploads-2.1.0}/safeuploads/exceptions.py +0 -0
  56. {safeuploads-2.0.0 → safeuploads-2.1.0}/safeuploads/inspectors/__init__.py +0 -0
  57. {safeuploads-2.0.0 → safeuploads-2.1.0}/safeuploads/inspectors/base.py +0 -0
  58. {safeuploads-2.0.0 → safeuploads-2.1.0}/safeuploads/inspectors/content_inspector.py +0 -0
  59. {safeuploads-2.0.0 → safeuploads-2.1.0}/safeuploads/inspectors/gzip_inspector.py +0 -0
  60. {safeuploads-2.0.0 → safeuploads-2.1.0}/safeuploads/inspectors/zip_inspector.py +0 -0
  61. {safeuploads-2.0.0 → safeuploads-2.1.0}/safeuploads/protocols.py +0 -0
  62. {safeuploads-2.0.0 → safeuploads-2.1.0}/safeuploads/py.typed +0 -0
  63. {safeuploads-2.0.0 → safeuploads-2.1.0}/safeuploads/utils.py +0 -0
  64. {safeuploads-2.0.0 → safeuploads-2.1.0}/safeuploads/validators/__init__.py +0 -0
  65. {safeuploads-2.0.0 → safeuploads-2.1.0}/safeuploads/validators/base.py +0 -0
  66. {safeuploads-2.0.0 → safeuploads-2.1.0}/safeuploads/validators/compression_validator.py +0 -0
  67. {safeuploads-2.0.0 → safeuploads-2.1.0}/safeuploads/validators/extension_validator.py +0 -0
  68. {safeuploads-2.0.0 → safeuploads-2.1.0}/safeuploads/validators/filename_validator.py +0 -0
  69. {safeuploads-2.0.0 → safeuploads-2.1.0}/safeuploads/validators/fit_validator.py +0 -0
  70. {safeuploads-2.0.0 → safeuploads-2.1.0}/safeuploads/validators/mime_validator.py +0 -0
  71. {safeuploads-2.0.0 → safeuploads-2.1.0}/safeuploads/validators/signature_validator.py +0 -0
  72. {safeuploads-2.0.0 → safeuploads-2.1.0}/safeuploads/validators/unicode_validator.py +0 -0
  73. {safeuploads-2.0.0 → safeuploads-2.1.0}/safeuploads/validators/windows_validator.py +0 -0
  74. {safeuploads-2.0.0 → safeuploads-2.1.0}/safeuploads/validators/xml_validator.py +0 -0
  75. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/__init__.py +0 -0
  76. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/conftest.py +0 -0
  77. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/fuzz/__init__.py +0 -0
  78. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/fuzz/helpers.py +0 -0
  79. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/fuzz/test_fuzz_config.py +0 -0
  80. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/fuzz/test_fuzz_filenames.py +0 -0
  81. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/fuzz/test_fuzz_images.py +0 -0
  82. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/fuzz/test_fuzz_zip.py +0 -0
  83. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/inspectors/__init__.py +0 -0
  84. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/inspectors/test_content_inspector.py +0 -0
  85. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/inspectors/test_gzip_inspector.py +0 -0
  86. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/inspectors/test_zip_inspector.py +0 -0
  87. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/test_audit.py +0 -0
  88. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/test_config.py +0 -0
  89. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/test_config_validation.py +0 -0
  90. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/test_enums.py +0 -0
  91. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/test_exceptions.py +0 -0
  92. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/test_fastapi_integration.py +0 -0
  93. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/test_performance.py +0 -0
  94. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/test_utils.py +0 -0
  95. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/test_validate_activity_file.py +0 -0
  96. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/test_validate_gzip_file.py +0 -0
  97. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/validators/__init__.py +0 -0
  98. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/validators/test_compression_validator.py +0 -0
  99. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/validators/test_extension_validator.py +0 -0
  100. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/validators/test_filename_validator.py +0 -0
  101. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/validators/test_fit_validator.py +0 -0
  102. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/validators/test_mime_validator.py +0 -0
  103. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/validators/test_signature_validator.py +0 -0
  104. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/validators/test_unicode_validator.py +0 -0
  105. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/validators/test_windows_validator.py +0 -0
  106. {safeuploads-2.0.0 → safeuploads-2.1.0}/tests/validators/test_xml_validator.py +0 -0
@@ -16,7 +16,7 @@ Where this skill and any of them disagree, they win.
16
16
  - Never loosen a default (limit, allow-list, block-list, ratio, depth) to make a test or one caller pass; ask first.
17
17
  - Keep the filename check order: Unicode sanitization, then Windows reserved names, then the remaining normalization.
18
18
  - Raise a `safeuploads.exceptions` subclass with its `ErrorCode`; the message is safe to show a caller and never contains file contents or filesystem paths.
19
- - Blocking work (archive inspection, hashing, magic) stays off the event loop; use the existing worker-thread offload and `executor` option.
19
+ - Blocking work (archive inspection, hashing, magic) stays off the event loop: async methods use the existing worker-thread offload and `executor` option; `*_sync` methods run it in the calling thread and keep the event-loop guard.
20
20
  - A released `ErrorCode` never changes meaning; a new failure mode gets a new code.
21
21
  - A new public name goes in `safeuploads/__init__.py` (`__all__`), `docs/api.md`, the README example if it changes usage, and `CHANGELOG.md`.
22
22
  - A rename, removal, signature change, default change that accepts or rejects different input, or `ErrorCode` change is breaking: mark it `!` and say so in the changelog.
@@ -92,7 +92,7 @@ uv run pytest
92
92
  - Configuration changes go through `FileSecurityConfig` and `SecurityLimits`, and a new field updates their cross-field validation.
93
93
  - Failures raise the exceptions in `safeuploads/exceptions.py` (with a stable `ErrorCode`), never a bare `ValueError` or `Exception`.
94
94
  - The order of filename checks is significant: Unicode sanitization first, Windows reserved names before other normalization.
95
- - Blocking inspection never runs on the event loop; it goes through `FileValidator`'s worker-thread offload.
95
+ - Blocking inspection never runs on the event loop: the async `validate_*_file` methods offload it to a worker thread, and the `*_sync` methods run it in the calling thread and refuse to run on an event-loop thread.
96
96
  - XML is parsed only through `defusedxml` (XXE-safe); archives are inspected without extracting to disk and under size, ratio, depth and entry limits.
97
97
  - Never build a filesystem path from an uploaded filename.
98
98
  - Logging uses `logging.getLogger(__name__)` and the `SecurityAuditLogger`; never log file contents.
@@ -4,6 +4,21 @@ All notable changes to this project are documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [2.1.0] - 2026-11-05
8
+
9
+ ### Added
10
+
11
+ - `FileValidator.validate_image_file_sync`, `validate_zip_file_sync`, `validate_activity_file_sync` and `validate_gzip_file_sync`: synchronous counterparts of the async `validate_*_file` methods, each taking `(file: SeekableFile, filename: str)` and returning the sanitized filename as `str`. They run the same checks, order, exceptions, error codes and audit events and block the calling thread without the worker-thread offload (the `executor` option applies only to the async methods); called on a thread with a running asyncio event loop they raise `RuntimeError` before reading the file.
12
+
13
+ ### Changed
14
+
15
+ - Shortened the uv `exclude-newer` cooldown from 30 to 7 days.
16
+ - Updated all dependency groups to their latest versions.
17
+
18
+ ### Security
19
+
20
+ - `validate_image_file` reads the 8 KB header and the content-analysis sample until the requested size or end of file. An upload object whose `read(n)` returned fewer than `n` bytes before its end left the rest of the sample unscanned, so a polyglot signature past the first short read was accepted.
21
+
7
22
  ## [2.0.0] - 2026-11-01
8
23
 
9
24
  ### Added
@@ -192,6 +207,7 @@ Initial release.
192
207
  - **MIME type verification:** magic-number validation for common file types.
193
208
  - Framework-agnostic async validation, a rich exception hierarchy with machine-readable error codes, secure defaults, and full type hints.
194
209
 
210
+ [2.1.0]: https://github.com/endurain-project/safeuploads/compare/v2.0.0...v2.1.0
195
211
  [2.0.0]: https://github.com/endurain-project/safeuploads/compare/v1.1.3...v2.0.0
196
212
  [1.1.3]: https://github.com/endurain-project/safeuploads/compare/v1.1.2...v1.1.3
197
213
  [1.1.2]: https://github.com/endurain-project/safeuploads/compare/v1.1.1...v1.1.2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: safeuploads
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Summary: A comprehensive file security system for validating uploads and preventing attacks
5
5
  Author-email: João Vitória Silva <joao@endurain.com>
6
6
  License: MIT
@@ -26,7 +26,7 @@ Secure file upload validation for Python 3.13+ applications. Catches dangerous f
26
26
 
27
27
  ## Features
28
28
 
29
- - **Framework-agnostic** async validation (FastAPI, generic)
29
+ - **Framework-agnostic** async and sync validation (FastAPI, generic)
30
30
  - Filename sanitization and Unicode security checks
31
31
  - Extension validation with configurable allow/block lists
32
32
  - ZIP bomb detection, nested archive inspection, and recursive structure protection
@@ -71,6 +71,19 @@ async def upload_image(file: UploadFile):
71
71
  return {"status": "success", "filename": file.filename}
72
72
  ```
73
73
 
74
+ For synchronous code or worker threads, use the `*_sync` methods. They block the calling thread and return the sanitized filename:
75
+
76
+ ```python
77
+ from safeuploads import FileValidator
78
+
79
+ validator = FileValidator()
80
+
81
+ with open("activity.gpx", "rb") as fh:
82
+ safe_name = validator.validate_activity_file_sync(fh, "activity.gpx")
83
+ ```
84
+
85
+ Calling a `*_sync` method on a thread running an asyncio event loop raises `RuntimeError`; await the async method there instead.
86
+
74
87
  ## Configuration
75
88
 
76
89
  ```python
@@ -12,7 +12,7 @@ Secure file upload validation for Python 3.13+ applications. Catches dangerous f
12
12
 
13
13
  ## Features
14
14
 
15
- - **Framework-agnostic** async validation (FastAPI, generic)
15
+ - **Framework-agnostic** async and sync validation (FastAPI, generic)
16
16
  - Filename sanitization and Unicode security checks
17
17
  - Extension validation with configurable allow/block lists
18
18
  - ZIP bomb detection, nested archive inspection, and recursive structure protection
@@ -57,6 +57,19 @@ async def upload_image(file: UploadFile):
57
57
  return {"status": "success", "filename": file.filename}
58
58
  ```
59
59
 
60
+ For synchronous code or worker threads, use the `*_sync` methods. They block the calling thread and return the sanitized filename:
61
+
62
+ ```python
63
+ from safeuploads import FileValidator
64
+
65
+ validator = FileValidator()
66
+
67
+ with open("activity.gpx", "rb") as fh:
68
+ safe_name = validator.validate_activity_file_sync(fh, "activity.gpx")
69
+ ```
70
+
71
+ Calling a `*_sync` method on a thread running an asyncio event loop raises `RuntimeError`; await the async method there instead.
72
+
60
73
  ## Configuration
61
74
 
62
75
  ```python
@@ -16,7 +16,7 @@ Secure file upload validation for Python 3.13+ applications. Catches dangerous f
16
16
 
17
17
  ## Features
18
18
 
19
- - **Framework-agnostic** async validation (FastAPI, generic)
19
+ - **Framework-agnostic** async and sync validation (FastAPI, generic)
20
20
  - Filename sanitization and Unicode security checks
21
21
  - Extension validation with configurable allow/block lists
22
22
  - ZIP bomb detection, nested archive inspection, and recursive structure protection
@@ -61,6 +61,19 @@ async def upload_image(file: UploadFile):
61
61
  return {"status": "success", "filename": file.filename}
62
62
  ```
63
63
 
64
+ For synchronous code or worker threads, use the `*_sync` methods. They block the calling thread and return the sanitized filename:
65
+
66
+ ```python
67
+ from safeuploads import FileValidator
68
+
69
+ validator = FileValidator()
70
+
71
+ with open("activity.gpx", "rb") as fh:
72
+ safe_name = validator.validate_activity_file_sync(fh, "activity.gpx")
73
+ ```
74
+
75
+ Calling a `*_sync` method on a thread running an asyncio event loop raises `RuntimeError`; await the async method there instead.
76
+
64
77
  ## Configuration
65
78
 
66
79
  ```python
@@ -38,7 +38,7 @@ safeuploads/
38
38
 
39
39
  | Component | Responsibility |
40
40
  |---|---|
41
- | `FileValidator` | Orchestrates validation for each file type. Manages streaming, resource monitoring, audit events, offloads blocking inspection to a worker thread (or a configured `executor`), and delegates to validators/inspectors. Exposes them as `filename_validator`, `signature_validator`, `mime_validator` and the other validator attributes; `magic_mime` and `magic_available` are properties that delegate to `mime_validator`. |
41
+ | `FileValidator` | Orchestrates validation for each file type. Manages streaming, resource monitoring and audit events, and delegates to validators/inspectors. The async methods offload blocking inspection to a worker thread (or a configured `executor`); the `*_sync` methods run the same pipeline in the calling thread and refuse to run on an event-loop thread. Exposes them as `filename_validator`, `signature_validator`, `mime_validator` and the other validator attributes; `magic_mime` and `magic_available` are properties that delegate to `mime_validator`. |
42
42
  | `FileSecurityConfig` | Centralizes all configuration: allowed MIME types, extensions, blocked extensions, Unicode characters, and Windows reserved names. |
43
43
  | `SecurityLimits` | Holds numeric thresholds: file sizes, compression ratios, timeouts, entry limits, resource caps. |
44
44
  | `BaseValidator` | Abstract base class; validators inherit and implement `validate()`. |
@@ -61,6 +61,8 @@ safeuploads/
61
61
 
62
62
  ## Validation Pipelines
63
63
 
64
+ The `validate_*_file_sync` methods run the identical steps listed below in the calling thread, without the worker-thread offload.
65
+
64
66
  ### Image Validation (`validate_image_file`)
65
67
 
66
68
  ```
@@ -400,7 +402,7 @@ and content threats, are `FileValidationError` subclasses.
400
402
  ## Audit Event Flow
401
403
 
402
404
  ```
403
- FileValidator.validate_*() (_run_validation)
405
+ FileValidator.validate_*() (_validation_run)
404
406
  │
405
407
  ├── bind_correlation_id() # reuses the host's ID, if set
406
408
  ├── audit.start() # VALIDATION_START
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "safeuploads"
3
- version = "2.0.0"
3
+ version = "2.1.0"
4
4
  description = "A comprehensive file security system for validating uploads and preventing attacks"
5
5
  authors = [
6
6
  {name = "João Vitória Silva",email = "joao@endurain.com"}
@@ -42,7 +42,7 @@ build-backend = "hatchling.build"
42
42
 
43
43
  [tool.uv]
44
44
  required-version = "0.13.5"
45
- exclude-newer = "30 days"
45
+ exclude-newer = "7 days"
46
46
 
47
47
  [tool.ruff]
48
48
  line-length = 79