Skip to content

Supporting 50 MiB uploads raises DATA_UPLOAD_MAX_MEMORY_SIZE globally, letting an unauthenticated caller force each worker to buffer 50 MiB per request (pre-auth memory amplification) #301

Description

@erskingardner

Summary

To accept large raw-body audit-log uploads, the app raises Django's global in-memory request-body cap to the full dump size:

# config/settings.py:290
DATA_UPLOAD_MAX_MEMORY_SIZE = GOGGLES_MAX_DUMP_BYTES   # = 50 * 1024 * 1024 by default (settings.py:278)

This is needed because the non-multipart upload path reads the whole body into memory:

# forensics/views.py:3300-3301  (audit_bytes_from_request)
if request.body:
    return request.body, "", request.content_type or ""

But DATA_UPLOAD_MAX_MEMORY_SIZE is a global Django setting, not scoped to the upload endpoint. Django enforces it whenever request.body / request.POST is accessed on a non-multipart request — which includes every ordinary form POST in the app. Raising it from Django's 2.5 MB default to 50 MiB (a 20× increase) therefore relaxes the per-request memory ceiling for all endpoints, including unauthenticated ones.

Failure scenario

CsrfViewMiddleware reads request.POST on every POST to validate the CSRF token, and the Django login view (/accounts/login/) is an unauthenticated application/x-www-form-urlencoded form. For a urlencoded request, accessing request.POST reads the entire raw body into worker memory (up to DATA_UPLOAD_MAX_MEMORY_SIZE) before parsing and before the view/authentication runs.

So an unauthenticated attacker can:

POST /accounts/login/ HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 52428800

username=<~50 MiB of characters>&password=x

and force the handling gunicorn worker to buffer ~50 MiB in memory before any credential check. DATA_UPLOAD_MAX_NUMBER_FIELDS (default 1000) does not help — a single oversized field value stays under the field-count limit while still carrying the full 50 MiB.

The per-request MaxDumpSizeUploadHandler (views.py:3078-3097, installed at views.py:3287-3289) only bounds multipart file parts (receive_data_chunk); it does nothing for the non-multipart body path that DATA_UPLOAD_MAX_MEMORY_SIZE governs.

With the shipped gunicorn config (Dockerfile CMD: --workers 3 --threads 4), a modest number of concurrent large POSTs can pin roughly 12 × 50 MiB ≈ 600 MiB of resident memory with no credentials, and the reverse proxy already permits 50 MB bodies through (see #253).

Impact (MEDIUM)

  • Pre-authentication resource amplification on a sensitive internal forensics tool: ~20× the memory Django's default would allow, reachable by anyone who can hit the login page.
  • Bounded by worker/thread count and requires the attacker to actually transmit the bytes, so it is a moderate DoS vector rather than a severe one — but it is real and specific to this configuration, and it exists independently of request rate (so it is not addressed by request-rate limiting).

Why this is not a duplicate

No existing issue observes that supporting 50 MiB uploads forced the global in-memory POST cap up for every endpoint.

Suggested fix

Decouple the large-body allowance from the global cap. Options:

  1. Keep DATA_UPLOAD_MAX_MEMORY_SIZE at (or near) Django's default and have the upload view read the body via a size-bounded streaming reader / request.upload_handlers for the non-multipart path too, rather than request.body. The multipart path already spools to disk and is bounded by MaxDumpSizeUploadHandler; the raw-body path should get an equivalent bound instead of relaxing the global setting.
  2. Or require uploads to be multipart (so FILE_UPLOAD_MAX_MEMORY_SIZE + the existing handler govern them) and drop the global raise entirely.
  3. At minimum, cap DATA_UPLOAD_MAX_MEMORY_SIZE to a small value and enforce the 50 MiB dump ceiling only on the authenticated upload endpoint.

A regression test can POST an oversized urlencoded body to /accounts/login/ and assert it is rejected before the body is fully buffered.

Metadata

Metadata

Assignees

No one assigned

    Labels

    MEDIUMSeverity: important bug or performance issue with bounded impactsecurity

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions