API discloses other users UUIDs, enabling session forgery #57
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Severity: CRITICAL (amplifier for the default-secret-key issue)
The bug
The session cookie contains only a user UUID, signed with
CIRCA_SECRET_KEY. Separately, theAPI returns other users' UUIDs to any authenticated caller:
_evidence_to_dict→created_by(photos.py:271)_decision_to_dict→created_by(photos.py:372)_comment_to_dict→created_by(photos.py:450)_job_to_dict→created_by(jobs.py:41)The frontend even renders
c.created_by.slice(0, 8)as the comment author label.Why it matters
On its own, a known signing key is not immediately exploitable if the attacker must guess a
UUIDv4. This disclosure removes that obstacle entirely and turns it into a two-step operation:
GET /api/photos/{id}/decisions→ collectcreated_byvalues. Decisions are made byreviewers and admins, so the admin's UUID is very likely present.
This is what converts the default
CIRCA_SECRET_KEYfrom poor hygiene into practical admintakeover, and it is the piece not described in #14.
Fix
Stop returning raw user identifiers. Resolve
created_byto a display name at the serializerboundary — which is needed for the attribution work anyway — and return an opaque or omitted id.
If a client-side identifier is genuinely required (e.g. "is this my comment?"), return a
per-collection opaque id or a boolean
is_ownrather than the primary key.Note this also improves the UI: the review workspace currently shows UUID fragments as author
labels, which is developer debris in a family member's tool.
Done when
created_byUUID appears in any response bodyReferences
backend/app/api/routes/photos.py:271,372,450;jobs.py:41frontend/src/pages/ReviewWorkspacePage.tsx:313Related: #14 (fail fast on insecure config), #22 (attribution display names).
Fixed in
5f2cbdb. 144 tests passing.New
app/api/attribution.py. Evidence, decision, comment, and job responses now carrycreated_by_display_nameandis_owninstead of the raw user id.Two design points worth recording:
Batched resolution.
resolve_display_names()collects the distinct actor ids from a result setand issues one query, rather than resolving per row. The performance audit specifically noted
that the projection pattern means there are currently no N+1 patterns anywhere in the API — this
fix does not introduce the first one.
is_owninstead of comparing ids client-side. The UI needs to know "is this my comment?" tooffer deletion. Returning a boolean answers that without handing back an identifier, which is the
whole point of the issue.
Unresolvable actors degrade to
"Unknown"and system-generated rows to"System", both withoutleaking the id. Tested including the deleted-user case and an end-to-end assertion that the
reviewer's UUID appears nowhere in a comments response body.
This also does the security half of #107 (display names in the UI). #107 remains open for the
frontend work — rendering the names, and the decision history that currently shows no author at all.