A renderer performed a second operation with different permissions.
DRF’s AdminRenderer builds an HTML interface for CRUD-style APIs. When a write request returned 400 Bad Request, the renderer attempted to recover the page’s list or detail representation by temporarily treating the request as GET and calling the view’s get() handler.
A normal GET enters APIView.dispatch(), runs APIView.initial(), and checks the configured permissions. The renderer’s direct call did not repeat that dispatch sequence. A requester who was allowed to submit a write but denied GET could therefore trigger the GET handler while an invalid write response was being rendered.
The issue required a specific intersection of renderer and permission behavior.
- 01
AdminRendereris enabled for the view. - 02
The client negotiates the HTML representation, such as with
Accept: text/html. - 03
The configured permissions allow a write method but deny GET for the same requester.
- 04
The submitted write is invalid and produces a
400 Bad Requestresponse. - 05
The view’s GET representation contains data the requester should not receive.
If any of these conditions is absent, the specific disclosure path described by the advisory is not reached.
The simulated GET bypassed normal request initialization.
dispatch() → initial() → check_permissions() → get()override_method(GET) → get()# Simplified affected behavior
with override_method(view, request, "GET") as request:
response = view.get(request, *view.args, **view.kwargs)
data = response.data
The method override changed what the view observed as request.method, but calling view.get() directly was not equivalent to dispatching a new request. The initial permission gate had run for the original write method—not for the simulated GET.
A denied representation could appear inside an allowed error response.
In an affected configuration, a direct GET correctly returned a permission denial. An invalid POST rendered through AdminRenderer could still return a 400 HTML response containing data produced by that protected GET handler.
| Request | Expected | Affected behavior |
|---|---|---|
| Direct GET | Permission denied | Permission denied |
| Invalid POST + JSON | Validation errors | Validation errors |
| Invalid POST + AdminRenderer | Validation errors only | Could include GET representation |
The finding does not imply that every use of AdminRenderer leaked data. It depended on method-specific permissions and on sensitive content being returned by the GET representation.
Authorize the simulated GET before invoking its handler.
The upstream fix adds an explicit permission check inside the method-override context. If the simulated GET is denied, the renderer does not call view.get(); it retains the validation error data for the HTML response. If permission succeeds, the existing representation behavior remains available.
with override_method(view, request, "GET") as request:
try:
view.check_permissions(request)
except APIException:
data = validation_errors
else:
data = view.get(request, *args, **kwargs).data
Regression tests cover both forms of the intended outcome: authorized GET representations continue to render, while denied representations are absent and the validation error remains visible.
A minimal permission class makes the boundary observable.
A bounded local test can use a permission that allows POST and denies GET, a view with both handlers, and a deliberately invalid POST. No load testing or access to real data is required.
class PostOnlyPermission(BasePermission):
def has_permission(self, request, view):
return request.method == "POST"
class DemoView(APIView):
renderer_classes = (AdminRenderer, JSONRenderer)
permission_classes = (PostOnlyPermission,)
def get(self, request):
return Response({"content": "protected-test-value"})
Compare a direct GET with an invalid POST that requests HTML. On an affected version, the second response can contain the controlled test value despite the denied direct GET. Use synthetic content and an isolated development application only.
The advisory credits the report; the public patch credits its maintainer author.
The official advisory credits zainnadeem786 as reporter. The investigation isolated the renderer-specific call path, demonstrated the permission mismatch with both generic and minimal views, and described the conditions required for impact.
The public remediation is PR #10012, authored by project maintainer browniebroke. This article distinguishes reporter and patch-author roles explicitly.
Error rendering remains part of the authorization boundary.
- Calling a method handler directly is not equivalent to dispatching a new framework request.
- Renderers and serializers that perform secondary lookups must preserve the permission context of those operations.
- Regression tests should combine method-specific permissions with failure responses, not only successful requests.
- When an auxiliary representation cannot be authorized, rendering the original validation errors is a safer fallback.