Summary
A low-privilege authenticated user restricted to one camera can access snapshots from other cameras.
This is possible through a chain of two authorization problems:
/api/timeline returns timeline entries for cameras outside the caller's allowed camera set.
/api/events/{event_id}/snapshot-clean.webp declares Depends(require_camera_access) but never actually validates event.camera after looking up the event.
Together, this allows a restricted user to enumerate event IDs from unauthorized cameras and then fetch clean snapshots for those events.
Affected code
frigate/api/app.py
frigate/api/media.py
frigate/api/auth.py
Relevant logic:
/api/timeline is allow_any_authenticated() and does not filter by allowed_cameras
require_camera_access() returns immediately when camera_name is None
/api/events/{event_id}/snapshot-clean.webp uses Depends(require_camera_access) on a route without a camera_name parameter
event_snapshot_clean() loads the event and reads the snapshot file without calling require_camera_access(event.camera, request=request)
Root cause
The code assumes that adding Depends(require_camera_access) to any route is sufficient.
That assumption is false for routes that do not have a camera_name path parameter. In those cases, require_camera_access() returns early and performs no authorization check.
Separately, /api/timeline also fails to intersect results with the authenticated user's allowed camera set.
Proof of concept
Assume a user is authenticated but only authorized for camera front.
Step 1: enumerate unauthorized event IDs
Request timeline entries for another camera:
GET /api/timeline?camera=garage&limit=1 HTTP/1.1
Host: <frigate-host>
Authorization: Bearer <viewer-token>
Example:
curl -sk \
-H "Authorization: Bearer $VIEWER_JWT" \
"https://<frigate-host>:8971/api/timeline?camera=garage&limit=1"
Observe that the response contains entries for garage, including a source_id value corresponding to an event ID.
Step 2: retrieve the unauthorized snapshot
Use the leaked source_id as event_id:
GET /api/events/<event_id>/snapshot-clean.webp HTTP/1.1
Host: <frigate-host>
Authorization: Bearer <viewer-token>
Example:
curl -sk \
-H "Authorization: Bearer $VIEWER_JWT" \
"https://<frigate-host>:8971/api/events/<event_id>/snapshot-clean.webp" \
-o unauthorized.webp
The snapshot for the unauthorized camera is returned.
Impact
A restricted authenticated user can:
- enumerate activity on cameras they should not access
- recover unauthorized event IDs
- retrieve clean snapshots from those cameras
This is a cross-camera confidentiality breach and a clear authorization bypass.
Why this is new
This is not the previously reported export-based arbitrary file read issue. This is a separate access control flaw affecting timeline metadata and event media authorization.
Suggested fix
- In
event_snapshot_clean(), explicitly enforce:
await require_camera_access(event.camera, request=request)
immediately after loading the event.
-
Apply per-camera filtering to /api/timeline and /api/timeline/hourly using the authenticated user's allowed_cameras.
-
Audit all routes that use Depends(require_camera_access) without a camera_name route parameter, because the same pattern may affect other media endpoints.
Summary
A low-privilege authenticated user restricted to one camera can access snapshots from other cameras.
This is possible through a chain of two authorization problems:
/api/timelinereturns timeline entries for cameras outside the caller's allowed camera set./api/events/{event_id}/snapshot-clean.webpdeclaresDepends(require_camera_access)but never actually validatesevent.cameraafter looking up the event.Together, this allows a restricted user to enumerate event IDs from unauthorized cameras and then fetch clean snapshots for those events.
Affected code
frigate/api/app.pyfrigate/api/media.pyfrigate/api/auth.pyRelevant logic:
/api/timelineisallow_any_authenticated()and does not filter byallowed_camerasrequire_camera_access()returns immediately whencamera_name is None/api/events/{event_id}/snapshot-clean.webpusesDepends(require_camera_access)on a route without acamera_nameparameterevent_snapshot_clean()loads the event and reads the snapshot file without callingrequire_camera_access(event.camera, request=request)Root cause
The code assumes that adding
Depends(require_camera_access)to any route is sufficient.That assumption is false for routes that do not have a
camera_namepath parameter. In those cases,require_camera_access()returns early and performs no authorization check.Separately,
/api/timelinealso fails to intersect results with the authenticated user's allowed camera set.Proof of concept
Assume a user is authenticated but only authorized for camera
front.Step 1: enumerate unauthorized event IDs
Request timeline entries for another camera:
Example:
Observe that the response contains entries for
garage, including asource_idvalue corresponding to an event ID.Step 2: retrieve the unauthorized snapshot
Use the leaked
source_idasevent_id:Example:
The snapshot for the unauthorized camera is returned.
Impact
A restricted authenticated user can:
This is a cross-camera confidentiality breach and a clear authorization bypass.
Why this is new
This is not the previously reported export-based arbitrary file read issue. This is a separate access control flaw affecting timeline metadata and event media authorization.
Suggested fix
event_snapshot_clean(), explicitly enforce:immediately after loading the event.
Apply per-camera filtering to
/api/timelineand/api/timeline/hourlyusing the authenticated user'sallowed_cameras.Audit all routes that use
Depends(require_camera_access)without acamera_nameroute parameter, because the same pattern may affect other media endpoints.