Async bulk API writes store plaintext v2 API tokens in Job.data, readable by any core.view_job holder
NetBox Edition
NetBox Community
NetBox Version
v4.7.1
Python Version
3.12
Steps to Reproduce
- As a user with permission to create tokens, POST a list of token payloads to /api/users/tokens/?background=true. The request returns 202 with a job reference.
- As a different user whose only permission is core.view_job, GET /api/core/jobs/ or /api/core/jobs//.
- Read data.data[].token — it's the full 40-character plaintext token.
Expected Behavior
A v2 token's plaintext exists only in memory during the creating request and is returned once in that response. Nothing server-side persists it — only an HMAC digest is stored. That invariant should hold no matter which code path created the token.
Observed Behavior
The background path writes the full serialized creation response, plaintext token included, into Job.data, which is an ordinary JSON column. It sits there for the JOB_RETENTION window — 90 days by default, forever if set to 0 — and is served verbatim by the jobs API to anyone holding core.view_job.
Suspected Cause
Three pieces that are each reasonable alone:
- TokenSerializer (netbox/users/api/serializers_/tokens.py:62-77) stashes the plaintext on the request during create() and re-injects it in to_representation(), so it survives the re-fetch that bulk create does. That's deliberate and correct for the synchronous path.
- AsyncAPIJob.run() (netbox/netbox/jobs.py:396-399) captures the action's response wholesale: self.job.data = {'status_code': ..., 'data': response.data}. It has no notion that a response body might contain a secret.
- JobViewSet (netbox/core/api/views.py:67) exposes data through JobSerializer (netbox/core/api/serializers_/jobs.py:35). The queryset is restricted, but core.view_job granted without constraints means every job.
TokenViewSet is a plain NetBoxModelViewSet, which picks up BackgroundOperationMixin and BulkCreateModelMixin (netbox/netbox/api/viewsets/init.py:160-174), so ?background=true works there with nothing opting it in.
Proposed Fix
Block background processing for TokenViewSet. The async path fundamentally can't deliver a write-once secret to the caller — a 202 has nowhere to put it except the job record, which is the problem — so token creation shouldn't be backgroundable in the first place.
Scrubbing the token field inside AsyncAPIJob would also work but is the more fragile option: it puts knowledge of which fields are secret in the wrong place, and it silently breaks if another serializer starts returning a write-once value.
Source: netbox-community/netbox