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:
- 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.
- Or require uploads to be multipart (so
FILE_UPLOAD_MAX_MEMORY_SIZE + the existing handler govern them) and drop the global raise entirely.
- 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.
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:
This is needed because the non-multipart upload path reads the whole body into memory:
But
DATA_UPLOAD_MAX_MEMORY_SIZEis a global Django setting, not scoped to the upload endpoint. Django enforces it wheneverrequest.body/request.POSTis 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
CsrfViewMiddlewarereadsrequest.POSTon every POST to validate the CSRF token, and the Django login view (/accounts/login/) is an unauthenticatedapplication/x-www-form-urlencodedform. For a urlencoded request, accessingrequest.POSTreads the entire raw body into worker memory (up toDATA_UPLOAD_MAX_MEMORY_SIZE) before parsing and before the view/authentication runs.So an unauthenticated attacker can:
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 atviews.py:3287-3289) only bounds multipart file parts (receive_data_chunk); it does nothing for the non-multipart body path thatDATA_UPLOAD_MAX_MEMORY_SIZEgoverns.With the shipped gunicorn config (
DockerfileCMD:--workers 3 --threads 4), a modest number of concurrent large POSTs can pin roughly12 × 50 MiB ≈ 600 MiBof resident memory with no credentials, and the reverse proxy already permits 50 MB bodies through (see #253).Impact (MEDIUM)
Why this is not a duplicate
/accounts/login/or/admin/for a sensitive forensics tool #277 (no brute-force / rate-limit on/accounts/login/and/admin/) is about credential-guessing request rate. This issue is about per-request memory volume and holds even at one request in flight.str.splitmaterialization / JSON nesting prescan insideingest), which requires a valid upload token. This vector is unauthenticated and on unrelated form endpoints.request_body max_size) permits the 50 MiB to reach Django; it does not bound the resulting in-memory buffering.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:
DATA_UPLOAD_MAX_MEMORY_SIZEat (or near) Django's default and have the upload view read the body via a size-bounded streaming reader /request.upload_handlersfor the non-multipart path too, rather thanrequest.body. The multipart path already spools to disk and is bounded byMaxDumpSizeUploadHandler; the raw-body path should get an equivalent bound instead of relaxing the global setting.FILE_UPLOAD_MAX_MEMORY_SIZE+ the existing handler govern them) and drop the global raise entirely.DATA_UPLOAD_MAX_MEMORY_SIZEto 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.