Summary
PacerFetchRequestViewSet serves PacerFetchQueue, which records the PACER purchases made by our users. Due to insufficient authorization constraints, it was possible for an attacker to alter or delete the requests of other users, including reassigning the fetch requests to other users.
By design this API created an anonymous public log of every request — such requests were not considered private and the developer convenience of seeing requests without authentication was determined to outweigh the risk of publicly accessible logs. In resolving this vulnerability, we determined that it is best to make previous requests private, and we've implemented that change in version 4.7 of the API.
Severity
High (CVSS 3.1: 7.1)
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L
- Attack Vector: Network -- plain HTTP requests to a public REST API.
- Attack Complexity: Low -- primary keys are sequential/enumerable integers; no race condition or special conditions.
- Privileges Required: Low -- every part of the actual impact (tampering, cancellation, ownership reassignment) required an authenticated account (trivial self-registration). The unauthenticated read path disclosed no identity/ownership data, so it carried no confidentiality impact of its own.
- User Interaction: None -- fully automatable, no victim action required.
- Scope: Unchanged -- impact stayed within the CourtListener application's own data/authorization boundary.
- Confidentiality Impact: None -- the exposed fields carried no user attribution. Anonymous read access to this shared activity log was intentional design, not a defect, and has been left unchanged by the fix.
- Integrity Impact: Low -- any authenticated account could modify another user's pending fetch-queue row or reassign its ownership,, but such modifications would be quickly identified and of limited consequence.
- Availability Impact: Low -- deleting a completed row had no functional effect (it's a disposable receipt once the underlying PACER purchase has already run); the only real availability impact was the ability to disrupt a still-pending, in-flight fetch before it executed.
Affected Component
cl/recap/views.py -- PacerFetchRequestViewSet (unscoped queryset, no object-level permission, no get_queryset() override)
cl/api/urls.py -- router registration (recap-fetch -> PacerFetchRequestViewSet) and v3/v4 route inclusion
cl/recap/api_serializers.py -- PacerFetchQueueSerializer (user hidden on both read and write, but no scoping anywhere)
cl/recap/filters.py -- PacerFetchQueueFilter (no user-scoping in the filter definition)
CWE
- CWE-862: Missing Authorization (no object-level permission class or ownership check existed for this viewset -- unlike sibling viewsets in the same codebase)
- CWE-639: Authorization Bypass Through User-Controlled Key (IDOR by primary key across list/retrieve/update/delete)
Impact
- Any self-registered account could delete or tamper with any other user's pending or historical fetch-queue row, with no ownership check. Deleting a completed row had no real functional impact (it's a disposable receipt once the underlying PACER purchase already ran), but deleting or
PUT-overwriting a still-pending row could disrupt or redirect an in-flight, real-money PACER purchase the victim had already initiated, or silently reassign that row's ownership to the attacker.
- No special permission was required for any part of this attack -- read access required nothing, and write/delete access required only ordinary free registration.
Remediation
Fixed by removing update/destroy support from the viewset entirely and scoping reads by ownership.
- No update or delete action is defined on the viewset at all, so DRF's router never wires up
PATCH/PUT/DELETE for it in the first place -- structurally simpler and more robust than gating those actions with a permission check, since the actions no longer exist to be reached by any caller, ever.
get_queryset() now scopes list/retrieve to the requesting user's own rows, closing the confidentiality/enumeration surface even though it was never identity-linked.
IsOwner (the same permission already used by VisualizationViewSet/JSONViewSet) is layered on top as defense in depth, so an object-level check still fires even if a future change to get_queryset() ever stops scoping correctly.
IsAuthenticatedOrReadOnly was replaced with IsAuthenticated: anonymous access is removed entirely, since there is nothing for a logged-out visitor to see once results are owner-scoped. AnonRateThrottle was dropped from throttle_classes for the same reason -- it can never trigger once every request must be authenticated.
Net effect: an account can only ever see and create its own fetch requests; nothing can be edited or deleted via the API once created, by anyone, including the owner.
Credit
This vulnerability was discovered and reported by bugbunny.ai.
Summary
PacerFetchRequestViewSetservesPacerFetchQueue, which records the PACER purchases made by our users. Due to insufficient authorization constraints, it was possible for an attacker to alter or delete the requests of other users, including reassigning the fetch requests to other users.By design this API created an anonymous public log of every request — such requests were not considered private and the developer convenience of seeing requests without authentication was determined to outweigh the risk of publicly accessible logs. In resolving this vulnerability, we determined that it is best to make previous requests private, and we've implemented that change in version 4.7 of the API.
Severity
High (CVSS 3.1: 7.1)
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:LAffected Component
cl/recap/views.py--PacerFetchRequestViewSet(unscopedqueryset, no object-level permission, noget_queryset()override)cl/api/urls.py-- router registration (recap-fetch->PacerFetchRequestViewSet) and v3/v4 route inclusioncl/recap/api_serializers.py--PacerFetchQueueSerializer(userhidden on both read and write, but no scoping anywhere)cl/recap/filters.py--PacerFetchQueueFilter(no user-scoping in the filter definition)CWE
Impact
PUT-overwriting a still-pending row could disrupt or redirect an in-flight, real-money PACER purchase the victim had already initiated, or silently reassign that row's ownership to the attacker.Remediation
Fixed by removing update/destroy support from the viewset entirely and scoping reads by ownership.
PATCH/PUT/DELETEfor it in the first place -- structurally simpler and more robust than gating those actions with a permission check, since the actions no longer exist to be reached by any caller, ever.get_queryset()now scopes list/retrieve to the requesting user's own rows, closing the confidentiality/enumeration surface even though it was never identity-linked.IsOwner(the same permission already used byVisualizationViewSet/JSONViewSet) is layered on top as defense in depth, so an object-level check still fires even if a future change toget_queryset()ever stops scoping correctly.IsAuthenticatedOrReadOnlywas replaced withIsAuthenticated: anonymous access is removed entirely, since there is nothing for a logged-out visitor to see once results are owner-scoped.AnonRateThrottlewas dropped fromthrottle_classesfor the same reason -- it can never trigger once every request must be authenticated.Net effect: an account can only ever see and create its own fetch requests; nothing can be edited or deleted via the API once created, by anyone, including the owner.
Credit
This vulnerability was discovered and reported by bugbunny.ai.