Skip to content

security: review hardening (password ACL, credential masking, FFI bounds) - #292

Merged
manupawickramasinghe merged 6 commits into
devfrom
sec/review-hardening
Oct 20, 2026
Merged

manupawickramasinghe merged 6 commits into
devfrom
sec/review-hardening

Conversation

@manupawickramasinghe

Copy link
Copy Markdown
Member

Summary

This is a security review of dev, covering the API auth and roles, file-serving and upload paths, SSRF, go2rtc/ffmpeg argument building, credential exposure, the WebSocket classifier, the Rust FFI wrappers and the fork's Docker build. It fixes the clear issues with small changes, each covered by tests. Anything debatable, or anything that would change behaviour, is listed under recommendations and left unchanged.

Two of the commits are clean cherry-picks (-x) of upstream security fixes that dev had not picked up yet. Both are already on sync/upstream-0.18, so they should be no-ops when that branch is integrated.

Findings

# Severity Location Issue Fix / recommendation
1 High frigate/api/auth.py update_password Only the literal viewer role was blocked from changing another user's password. Any custom role (e.g. neighbor) could reset the admin password (privilege escalation). Fixed: backport of upstream ad79e66 (blakeblackshear#24057). Only admin can target other accounts. The endpoint is now rate-limited. Tests in test_http_password.py
2 Medium frigate/api/notification.py register_notifications Push subscription endpoint URL was stored unvalidated, then POSTed to by the webpush sender (SSRF to internal hosts/ports). Fixed: same backport. Requires a public https URL on port 443, no credentials, no private/internal host. The conflict was resolved by keeping the fork's remote-user handling. test_webpush_registration.py, test_notification_api.py
3 Medium frigate/api/review.py DELETE /review/{id}/viewed No camera access check: camera-restricted users could change review state for cameras they cannot see. Fixed: same backport
4 Medium frigate/api/review.py POST /review/summarize/... The GenAI summary correlates activity across all cameras. Fixed: backport of upstream fc79aea (blakeblackshear#24056), require_full_camera_access. Tests in test_http_camera_access.py
5 Medium frigate/util/builtin.py clean_camera_user_pass Masking regex only matched usernames in [a-zA-Z0-9_-]. rtsp://john.doe:pw@…, rtsp://admin%40x:pw@…, rtsp://:pw@… and user=john.doe&password=… were returned unmasked. This affected log lines and the camera paths, ffmpeg_cmds and go2rtc streams in GET /config, which every authenticated role can read. Fixed: masking uses broader patterns (REGEX_REDACT_*). The pattern used by escape_special_characters is unchanged, so the ffmpeg command line is built exactly as before. Tests in test_camera_pw.py
6 Medium frigate/api/app.py GET /config go2rtc.rtsp.password, go2rtc.api.password and go2rtc.webrtc.ice_servers[].credential were returned verbatim to all roles. Fixed: replaced with the redaction sentinel for non-admin roles. Admin output is unchanged, so editing still works. Tests in test_http_app.py
7 Medium frigate/util/frame_rs.py yuv420_crop_repack_rust src_w/src_h were passed to Rust unchecked against the frame, so a smaller frame was read out of bounds. Fixed: validates the buffer size and that the crop lies inside the frame, raising ValueError. The caller falls back to numpy. test_frame_rs_bounds.py
8 Low frame_rs.py yuv420_to_3channel_rust, yuv420_region_to_rgb_resize_rust On odd sizes, non-2-D input, a degenerate region or a zero destination, Rust returns early and the wrapper returned its uninitialized np.empty buffer as a frame. An odd detect.width would feed heap garbage to the detector. Fixed: these cases now raise ValueError so the existing Python fallback runs. test_frame_rs_bounds.py
9 Low frigate/detectors/npu_service_runner.py Response header n*dim*4 (and error length) drove an unbounded allocation. The stats body was read without a limit. Fixed: response must match the request batch and have 0 < dim <= 65536. Error and stats bodies are capped at 64 KiB. test_npu_service_runner.py
10 Low docker/ncnn-vulkan/Dockerfile pip install --no-deps ncnn portalocker tqdm was unpinned. Fixed: pinned to the versions the current image ships.
11 Low frigate/api/fastapi_app.py check_csrf Only checks that X-CSRF-Token is present. This works as a custom-header CSRF defence only because browsers can't send it cross-origin without CORS. Combined with SameSite unset (Lax default), it is acceptable but brittle. Recommendation: keep as is (upstream design), or also verify Origin/Sec-Fetch-Site for unsafe methods.
12 Low frigate/api/auth.py jwt.decode No explicit algorithm allow-list. joserfc's defaults plus a symmetric OctKey make none/RS confusion not exploitable, but it is implicit. Recommendation: pass algorithms=["HS256"] (upstream code, so best done upstream).
13 Low frigate/embeddings/onnx/base_embedding.py _process_image requests.get(image) with no timeout or size limit for http* strings. No user-reachable caller passes a string today (all callers pass thumbnail bytes or arrays). Recommendation: drop URL support or add a timeout and size cap (upstream code).
14 Low frigate/api/classification.py /faces/{name}/register, /faces/recognize file.file.read() unbounded, no content-type check, decoded with OpenCV (decompression bombs). Admin-only, and nginx caps the body at 20 MB. Recommendation: cap the pixel count before decode.
15 Info frigate/api/export.py GET /cases, /cases/{id} Case names and descriptions are visible to camera-restricted users (metadata only, no media). Upstream design. Note only.
16 Info frigate/api/camera.py go2rtc add/delete Error paths log and return r.text from go2rtc, which may echo a stream source. Admin-only. Recommendation: run it through clean_camera_user_pass.
17 Info docker/ncnn-vulkan/Dockerfile FROM ghcr.io/blakeblackshear/frigate:stable is a mutable tag. The container runs as root (upstream design). Recommendation: pin by digest. Root left as-is.

Reviewed with no issues found

  • Admin-by-default and exemptions: I listed every route and its dependencies. Each route without a route-level dependency is either admin-by-default or checks require_camera_access inline.
  • Path handling: traversal handling via safe_join/sanitize_path_component (faces, classification, triggers, exports), the /preview/{file} thumbnail endpoint, and the media-auth URI classifier (rejects ./..).
  • Other API checks: the /labels cache (the access check runs before the cache lookup) and the go2rtc exec:/echo:/expr: filter (checked before and after variable substitution).
  • WebSocket classifier: fail-closed, with notification_test blocked.
  • JWT secret handling: generated with 128 hex characters and stored 0600.
  • Rust FFI: the other frame_rs wrappers, motion/rust_engine.py and detectors/rust_yolo.py were already bounds-checked.

Testing

  • ruff check and ruff format --check on frigate migrations docker *.py: clean.
  • python3 generate_api_auth_spec.py --check: up to date.
  • Full Python suite in frigate-ncnn-vulkan-npu:latest with freshly built Rust libs mounted over /opt/frigate: Ran 1042 tests, OK (skipped=1). The known-flaky WAL test passed on this run.
  • cargo test --release: frame-rs 33 passed, motion-rs 25 passed, yolo-rs 12 passed (no Rust changes).

No changes to test/upstream-0.18-rust-integration / #244.

🤖 Generated with Claude Code

NickM-27 and others added 6 commits October 20, 2026 05:14
* Make review user read status consistent with other APIs

* Validate URLs for web push endpoint

* Validate the role for a custom viewer, rate limit password changing

* Cleanup

(cherry picked from commit ad79e66)

Conflict in register_notifications resolved by keeping the fork's
remote-user handling and validating the subscription before storing it.
Fork test test_notification_api updated to send a well-formed subscription.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…th full camera access only (blakeblackshear#24056)

* Fix review summary analysis

* Add ability to scope based on full camera access

(cherry picked from commit fc79aea)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
clean_camera_user_pass only matched usernames made of [a-zA-Z0-9_-], so a
URL such as rtsp://john.doe:secret@cam or rtsp://:secret@cam, or an http
snapshot URL with user=john.doe&password=..., passed through unmasked.
That function masks ffmpeg/go2rtc log lines and the camera input paths,
ffmpeg_cmds and go2rtc streams returned by GET /config, which every
authenticated role can read. Redaction now uses broader patterns; the
narrow pattern used by escape_special_characters is unchanged so the
ffmpeg command line is built exactly as before.

GET /config also returned go2rtc's rtsp/api listener passwords and WebRTC
ICE server credentials verbatim. They are now replaced with the redaction
sentinel for non-admin roles; admins still receive them for editing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
yuv420_crop_repack_rust passed caller-supplied src_w/src_h to Rust without
checking them against the frame, so a frame smaller than claimed was read
out of bounds. yuv420_to_3channel_rust and yuv420_region_to_rgb_resize_rust
did not reject odd sizes, non-planar input, degenerate regions or a zero
destination, where Rust returns early and the wrapper handed back its
uninitialized np.empty buffer as a converted frame (e.g. an odd detect
width fed heap garbage to the detector). All three now raise ValueError,
which the callers already catch to fall back to the numpy/OpenCV path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The runner allocated n*dim*4 bytes (or dim bytes for an error) straight
from the response header, and the stats reader accumulated an unbounded
body. A broken or hostile peer at the configured endpoint could make the
embeddings process allocate gigabytes. The response must now match the
request batch size with a sane embedding width, and error/stats bodies
are capped at 64 KiB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Versions match what the current frigate-ncnn-vulkan-npu image ships.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@manupawickramasinghe
manupawickramasinghe merged commit 7b4a8f9 into dev Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants