Skip to content

fix: block dangerous file extension uploads in payload API#3302

Open
deacon-mp wants to merge 10 commits intomasterfrom
fix/payload-rce-code-injection
Open

fix: block dangerous file extension uploads in payload API#3302
deacon-mp wants to merge 10 commits intomasterfrom
fix/payload-rce-code-injection

Conversation

@deacon-mp
Copy link
Copy Markdown
Contributor

@deacon-mp deacon-mp commented Mar 16, 2026

Summary

The payload upload endpoint had no file extension validation, allowing .py, .pyc, .pyo, .so, and .dll files to be placed in data/payloads/ where they could be imported or executed server-side. This fix adds a _BLOCKED_EXTENSIONS set and _validate_payload_extension() in PayloadApi.post_payloads, called after sanitize_filename() before any disk I/O.

Security Impact

Severity: Critical — An authenticated attacker could upload a malicious .py file to data/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_EXTENSIONS frozenset (.py, .pyc, .pyo, .so, .dll) and _validate_payload_extension() called after sanitize_filename() in post_payloads; blocked uploads receive a 400 response with a descriptive error before any file is written to disk

Tests

  • tests/security/test_payload_extension_blocking.py: 13 tests, all passing

Test plan

  • Upload a .py file via the payload API — confirm a 400 error is returned and no file is written to disk
  • Upload a .so or .dll file — confirm the same rejection behavior
  • Upload a legitimate payload (.exe, .ps1, .sh) — confirm it is accepted and stored normally
  • Verify the payload listing and download endpoints are unaffected for existing payloads

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).
@deacon-mp deacon-mp requested a review from Copilot March 16, 2026 03:49
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +15 to +17
# 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'}
Comment on lines +63 to +66
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'
@deacon-mp deacon-mp requested a review from Copilot March 16, 2026 04:35
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_payloads before 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.

Comment on lines +10 to +63
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
Comment on lines +62 to +67
"""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}"
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_EXTENSIONS and _validate_payload_extension() to reject .py, .pyc, .pyo, .so, .dll uploads in PayloadApi.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
@sonarqubecloud
Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

@sonarqubecloud
Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@sonarqubecloud
Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

jlklos and others added 3 commits March 24, 2026 16:30
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_payloads after 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 jlklos self-requested a review March 25, 2026 21:32
Copy link
Copy Markdown
Contributor

@jlklos jlklos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants