fix: block dangerous file extension uploads in payload API#3302
fix: block dangerous file extension uploads in payload API#3302
Conversation
Uploading .py files via POST /api/v2/payloads followed by triggering their execution constitutes CWE-94 Remote Code Execution. Block .py/.pyc/.pyo/.so/.dll uploads in the payload API handler. LOCAL BRANCH ONLY — security disclosure pending
Adds 13 unit tests for _validate_payload_extension() verifying CWE-94 fix: blocked extensions (.py/.pyc/.pyo/.so/.dll), case-insensitivity, double extensions, and allowlist of safe types (.exe, .zip, .go, no-extension).
There was a problem hiding this comment.
Pull request overview
Adds server-side payload upload hardening by rejecting known executable extensions to mitigate RCE risk via uploaded files.
Changes:
- Introduces a blocked-extension constant and
_validate_payload_extension()in the payload upload handler. - Calls extension validation after filename sanitization and before any disk I/O.
- Adds security regression tests covering blocked/allowed extensions and case handling.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| app/api/v2/handlers/payload_api.py | Adds blocked extension list + validator; invokes validator in post_payloads before writing payloads. |
| tests/security/test_payload_extension_blocking.py | Adds regression tests ensuring blocked extensions raise 400 and common payloads remain allowed. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
app/api/v2/handlers/payload_api.py
Outdated
| # Extensions that could be executed server-side and must never be accepted as | ||
| # uploaded payloads (CWE-94 / Remote Code Execution mitigation). | ||
| _BLOCKED_EXTENSIONS = {'.py', '.pyc', '.pyo', '.so', '.dll'} |
| assert '.py' in _BLOCKED_EXTENSIONS | ||
| assert '.pyc' in _BLOCKED_EXTENSIONS | ||
| assert '.so' in _BLOCKED_EXTENSIONS | ||
| assert '.dll' in _BLOCKED_EXTENSIONS |
- Change _BLOCKED_EXTENSIONS from mutable set to frozenset to prevent accidental runtime modification of this security-sensitive denylist - Expand test_blocked_extensions_set to assert frozenset type and check all members including .pyo (previously omitted), matching the docstring claim of 'exactly the expected types'
There was a problem hiding this comment.
Pull request overview
Adds server-side validation to block uploading potentially executable payload file types to mitigate RCE risk via the payload upload endpoint.
Changes:
- Introduces a blocked extension set and a helper validator for payload filenames.
- Enforces extension validation in
PayloadApi.post_payloadsbefore any disk I/O. - Adds security regression tests covering blocked and allowed extensions, including case variations.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| app/api/v2/handlers/payload_api.py | Adds blocked-extension policy and enforces it during payload uploads. |
| tests/security/test_payload_extension_blocking.py | Adds regression tests for the extension-blocking behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| from app.api.v2.handlers.payload_api import _validate_payload_extension, _BLOCKED_EXTENSIONS | ||
|
|
||
|
|
||
| class TestPayloadExtensionBlocking: | ||
|
|
||
| def test_py_file_rejected(self): | ||
| with pytest.raises(web.HTTPBadRequest): | ||
| _validate_payload_extension('malicious.py') | ||
|
|
||
| def test_pyc_file_rejected(self): | ||
| with pytest.raises(web.HTTPBadRequest): | ||
| _validate_payload_extension('malicious.pyc') | ||
|
|
||
| def test_pyo_file_rejected(self): | ||
| with pytest.raises(web.HTTPBadRequest): | ||
| _validate_payload_extension('malicious.pyo') | ||
|
|
||
| def test_so_file_rejected(self): | ||
| with pytest.raises(web.HTTPBadRequest): | ||
| _validate_payload_extension('exploit.so') | ||
|
|
||
| def test_dll_file_rejected(self): | ||
| with pytest.raises(web.HTTPBadRequest): | ||
| _validate_payload_extension('evil.dll') | ||
|
|
||
| def test_uppercase_extension_rejected(self): | ||
| with pytest.raises(web.HTTPBadRequest): | ||
| _validate_payload_extension('malicious.PY') | ||
|
|
||
| def test_mixed_case_extension_rejected(self): | ||
| with pytest.raises(web.HTTPBadRequest): | ||
| _validate_payload_extension('malicious.Py') | ||
|
|
||
| def test_exe_allowed(self): | ||
| """Agent binaries (.exe) must still be uploadable.""" | ||
| _validate_payload_extension('sandcat.exe') # should not raise | ||
|
|
||
| def test_elf_allowed(self): | ||
| _validate_payload_extension('sandcat-linux') # no extension — should not raise | ||
|
|
||
| def test_zip_allowed(self): | ||
| _validate_payload_extension('payloads.zip') | ||
|
|
||
| def test_go_allowed(self): | ||
| _validate_payload_extension('manx.go') | ||
|
|
||
| def test_double_extension_py_blocked(self): | ||
| """Files like 'legit.txt.py' must still be rejected.""" | ||
| with pytest.raises(web.HTTPBadRequest): | ||
| _validate_payload_extension('legit.txt.py') | ||
|
|
||
| def test_blocked_extensions_set(self): | ||
| """Verify the constant is a frozenset and contains exactly the expected types.""" | ||
| from app.api.v2.handlers.payload_api import _BLOCKED_EXTENSIONS as _BLK |
| """Verify the constant is a frozenset and contains exactly the expected types.""" | ||
| from app.api.v2.handlers.payload_api import _BLOCKED_EXTENSIONS as _BLK | ||
| assert isinstance(_BLK, frozenset), "_BLOCKED_EXTENSIONS must be a frozenset" | ||
| expected = frozenset({'.py', '.pyc', '.pyo', '.so', '.dll'}) | ||
| assert _BLK == expected, ( | ||
| f"_BLOCKED_EXTENSIONS mismatch: got {_BLK}, expected {expected}" |
…o avoid brittleness
There was a problem hiding this comment.
Pull request overview
Adds server-side validation to block uploading potentially executable payload file types to data/payloads/, mitigating an RCE vector in the v2 payload upload endpoint.
Changes:
- Introduces
_BLOCKED_EXTENSIONSand_validate_payload_extension()to reject.py,.pyc,.pyo,.so,.dlluploads inPayloadApi.post_payloads. - Adds security regression tests for the new extension-blocking behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| app/api/v2/handlers/payload_api.py | Adds extension blocklist + validation call during payload upload. |
| tests/security/test_payload_extension_blocking.py | Adds unit-style tests for the extension validation helper/constant. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import pytest | ||
| from aiohttp import web | ||
|
|
||
| from app.api.v2.handlers.payload_api import _validate_payload_extension, _BLOCKED_EXTENSIONS |
|
❌ The last analysis has failed. |
|
❌ The last analysis has failed. |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
|
❌ The last analysis has failed. |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
|
There was a problem hiding this comment.
Pull request overview
Adds server-side protection to prevent uploading potentially executable file types into data/payloads/ via the API, reducing RCE risk from malicious payload uploads.
Changes:
- Introduces a blocked-extension allow/deny check (
_BLOCKED_EXTENSIONS,_validate_payload_extension) in the v2 payload upload handler. - Enforces the extension validation in
PayloadApi.post_payloadsafter filename sanitization and before any disk I/O. - Adds regression unit tests for the new validator and constant.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| app/api/v2/handlers/payload_api.py | Adds blocked extension constant + validator and calls it during payload upload. |
| tests/security/test_payload_extension_blocking.py | Adds unit tests asserting blocked extensions raise HTTPBadRequest and allowed extensions pass. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
jlklos
left a comment
There was a problem hiding this comment.
Tested and verified that files with blocked file extensions are unable to be uploaded through the payload API, and verified that those files are not written to disk. Files with allowed file extensions are still able to be uploaded without issue.



Summary
The payload upload endpoint had no file extension validation, allowing
.py,.pyc,.pyo,.so, and.dllfiles to be placed indata/payloads/where they could be imported or executed server-side. This fix adds a_BLOCKED_EXTENSIONSset and_validate_payload_extension()inPayloadApi.post_payloads, called aftersanitize_filename()before any disk I/O.Security Impact
Severity: Critical — An authenticated attacker could upload a malicious
.pyfile todata/payloads/and trigger server-side execution through import mechanisms or plugin loading, achieving remote code execution on the Caldera server with the server process's privileges.Changes
app/api/v2/handlers/payload_api.py: Added_BLOCKED_EXTENSIONSfrozenset (.py,.pyc,.pyo,.so,.dll) and_validate_payload_extension()called aftersanitize_filename()inpost_payloads; blocked uploads receive a 400 response with a descriptive error before any file is written to diskTests
tests/security/test_payload_extension_blocking.py: 13 tests, all passingTest plan
.pyfile via the payload API — confirm a 400 error is returned and no file is written to disk.soor.dllfile — confirm the same rejection behavior.exe,.ps1,.sh) — confirm it is accepted and stored normally