Files
2026-09-09 21:44:05 -05:00

278 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Security Audit — oko livestream/camera pipeline
**Audited scope:** all Go modules (`frame`, `runCam`, `mofin`, `terp`, `coordinator`, `livestream-cache`, `oko-run`), shell/launcher scripts, YAML configs, and everything tracked in the git repository.
**Date:** 2026-08-28
**Method:** full source review + targeted exploit verification (path traversal, malformed-clip crash, committed-secret history scan).
---
## Executive summary
The oko pipeline moves live/captured camera frames over plaintext TCP between never-authenticated daemons. Anyone who can reach any of the default `:808x`/`:8443` listeners can inject video clips, poison live feeds, crash daemons, or read live footage. The most severe problems are not crypto weaknesses but architectural ones: **no authentication anywhere, no input validation on anything received from the network, no resource limits, and secrets/footage committed to git.**
**Verified during this audit:**
- A single crafted TCP clip crashes `coordinator`, `mofin`, and `terp` (index-out-of-range panic in `Clip.Sublimate`).
- An attacker-controlled `SourceData` value lets a clip escape the storage directory (path traversal to arbitrary file writes).
- TLS private key + real surveillance footage are present in git history.
---
## Findings (most → least serious)
### 1. CRITICAL — No authentication on any network endpoint; all listeners bind `0.0.0.0`
**Location:**
- `mofin/main.go:28,41,54-73` — listener, default `:8083`
- `terp/main.go:27,47,59-102` — listener, default `:8081`
- `coordinator/main.go:41,86,98-113` — listener, default `:8082`
- `livestream-cache/main.go:256-325` — TCP ingest on `:8085`
- `livestream-cache/main.go:327-347` — HTTPS MJPEG server on `:8443`
- `run-stack.sh:58-87` — launches everything bound to all interfaces
**Description:** Every daemon listens on `:port` (all interfaces) and accepts any TCP connection. There is no token, no shared secret handshake, no TLS client-auth, no IP allowlist — nothing. The config comments ("over Tailscale") imply the intended boundary, but nothing enforces it; the ports are also reachable on any other interface (LAN, Docker bridges, etc.).
**Impact:**
- Remote clip injection: arbitrary video can be routed into the motion-detection / classifier / storage pipeline.
- Live-feed poisoning: spoofed frames overwrite legitimate cameras in the livestream cache (`livestream-cache/main.go:299-321`), so a viewer sees attacker-chosen images on a real camera stream.
- DoS: malicious peers can crash daemons and exhaust resources (findings #2, #3, #5).
- Data theft: if any listener is exposed, an attacker gets fuller access than the camera operator intends.
**Recommendation:** Require authentication on every ingest endpoint (HMAC-authenticated messages or an API token), bind to loopback/Tailscale interface explicitly (configurable per service), and consider TLS with client certificates for inter-node traffic.
---
### 2. CRITICAL — Remotely-triggerable crash: malformed Clip causes index-out-of-range panic in `Clip.Sublimate`
**Location:** `frame/frame.go:128-148` (`Sublimate`, indexing `clp.Guids[i]` at line 142 and `clp.Timestamps[i]` at line 143). `CheckLenCorrelations` exists at `frame/frame.go:151-166` but is **never called** on any receive path.
- Consumers that crash: `coordinator/main.go:172-180`, `mofin/main.go:100-108`, `terp/main.go:117-126,138`.
- `coordinator` additionally indexes `clp.Timestamps[0]` / `clp.Guids[0]` at `coordinator/main.go:193,217,327`.
**Description:** A `Clip` with a non-empty `PixelMats` but an empty/short `Guids` or `Timestamps` slice causes `Sublimate` to panic. These structs are decoded straight from the network via `gob` with zero validation.
**Impact:** One small crafted packet kills any ingest daemon (`coordinator`, `mofin`, `terp`). No authentication means any reachable host can take the entire recording pipeline down, repeatedly. This is a remote crash / availability vulnerability.
**Verified:** unit test reproduced `runtime error: index out of range [0] with length 0`.
**Recommendation:** Validate `Clip` length invariants (`CheckLenCorrelations`) immediately after decode on every receiver, bound the number of frames per clip, and recover from panics in connection handlers (`defer recover()`).
---
### 3. CRITICAL — Path traversal in `coordinator` → arbitrary file write outside storage
**Location:** `coordinator/main.go:191-196`:
```go
user, camera := splitSource(clp.SourceData) // line 191
...
clipDir := filepath.Join(storageDir, user, camera, dateShard, dirName)
if err := os.MkdirAll(clipDir, 0755); err != nil { ... }
```
`splitSource` (`coordinator/main.go:360-365`) does no sanitization; `SourceData` comes verbatim from the network.
**Description:** `filepath.Clean` preserves `..` elements that escape the root. A `SourceData` of e.g. `../../../../tmp/pwn--cam` resolves `clipDir` to something outside `storageDir` and the coordinator then `MkdirAll`s it and writes `clip.mp4` / `clip.json` inside.
**Impact:** Remote arbitrary file creation/overwrite anywhere the `coordinator` process can write. At minimum: fill the disk, clobber config/log files, plant files for operators to open, or stage payloads. Because the process runs as the service user (often `root` on Pis), this can become full host compromise.
**Verified:** `filepath.Join("storage", "../../../../tmp/pwn", "cam", ...)` resolves to `/home/tmp/pwn/cam/...`, comfortably outside storage.
**Recommendation:** Sanitize/whitelist `SourceData` (allow only `[a-zA-Z0-9_-/]`), strip `..` segments, and resolve+verify the final path is within `storageDir` (`EvalSymlinks` / prefix check) before any `MkdirAll`.
---
### 4. CRITICAL — TLS private key for the livestream HTTPS server is committed to git
**Location:** `livestream-cache/key.pem` and `livestream-cache/cert.pem` (tracked; present in *every* commit since added). Used at `livestream-cache/main.go:344` (`ListenAndServeTLS`).
**Description:** The `key.pem` is a live private key checked into the repository (mode `0644`).
**Impact:** Anyone with repo access (or the public history) can operate the server identity and MITM the live camera streams served over `:8443`; combined with finding #1, the HTTPS layer provides no actual security for this deployment. Self-signed, so browsers already show warnings.
**Recommendation:** Remove both files from the repo **and history** (`git filter-repo`), rotate the key immediately, generate certs at deploy time or store in a secrets manager, and `chmod 600` the key file.
---
### 5. CRITICAL — Real surveillance footage is committed to the git repository
**Location:** `coordinator/storage/oko/front/0/2026-06-30/*/clip.mp4` + `clip.json` (tracked). The JSON exposes `"source_data": "oko--front/0"`, timestamps, GUIDs, detections.
**Description:** Actual camera recordings of a camera labeled `front` are in the repo and history.
**Impact:** Permanent privacy/legal exposure of security-camera footage of people and property. Anyone who ever gets the repo gets the footage. This cannot be fixed by deleting the files — history must be rewritten.
**Recommendation:** Purge storage output from git history (`git filter-repo`), add `coordinator/storage/**` (and `logs/`) to `.gitignore`, and ensure no future clip data is committed.
---
### 6. HIGH — Unbounded resource consumption / DoS: no limits on decode size, connections, or goroutines
**Location:**
- Unbounded connection + goroutine per accept: `coordinator/main.go:98-113`, `mofin/main.go:52-73`, `terp/main.go:59-102`, `livestream-cache/main.go:269-279`.
- Unbounded `gob.Decode` with no stream/field size caps: `coordinator/main.go:172`, `mofin/main.go:67`, `terp/main.go:74`, `livestream-cache/main.go:288`.
- No read/write deadlines on any socket (slow-loris friendly).
- Per-clip processing goroutines with no bound on frames-per-clip: `terp/main.go:141-151` (one goroutine per frame × one per classifier), `mofin/main.go:79-88`.
- Livestream cache map grows per untrusted `SourceData` key until the TTL janitor runs (`livestream-cache/main.go:66-73,235-254`) — attacker can flood with millions of synthetic IDs in 30 s.
- `http.Server` has no `ReadHeaderTimeout`/`MaxHeaderBytes`/connection limits (`livestream-cache/main.go:332-336`).
**Impact:** Memory exhaustion (gob-decode bombs, large `PixelMats`, giant maps), CPU exhaustion (frame/classifier goroutines), and connection exhaustion from a handful of sockets. All remotely triggerable without auth.
**Recommendation:** Enforce max clip size / frame count / dimension limits, `io.LimitReader` on connections, per-connection deadlines, semaphore-bounded worker pools instead of unbounded goroutines, and sane `http.Server` timeouts/limits.
---
### 7. HIGH — Live camera streams (`/stream/...`) served over HTTPS with no authentication
**Location:** `livestream-cache/main.go:354-422` (`handleMJPEGStream`).
**Description:** Any unauthenticated client that can reach `:8443` can fetch live frames:
- `GET /stream/oko--front/0` → live MJPEG stream.
- Valid camera IDs are trivially enumerable (200 vs 404, `main.go:361-364`).
- No auth headers, cookies, or originating-IP restrictions.
- Because it's a plain `<img>`-loadable resource, any web page the victim visits can embed the stream (no CORS framing/canvas interplay blocks images), leaking the feed to third parties.
**Impact:** Anyone on the network can watch the cameras live — the very thing a home/office security system must prevent.
**Recommendation:** Require authentication (session cookie or per-camera token), serve on a Tailscale-only listener, hide whether cameras exist (uniform responses), and add `X-Frame-Options`/CSP framing controls.
---
### 8. HIGH — Encryption defaults off: clips are plaintext on the wire and at rest
**Location:**
- `runCam/main.go:39``-passphrase` defaults to `""` (no encryption).
- `mofin/main.go:33`, `terp/main.go:31`, `coordinator/main.go:43` — decrypt enabled only when passphrase set.
- All provided configs ship `passphrase: ""` or omit it (`config.yaml`, `cam-only.yaml:12-13`, `livestream-cache/config.yaml:5`, `run-stack.sh`).
- No TLS anywhere in the pipeline; transport is raw TCP + gob (`frame/frame.go:239-252`, `frame/network.go:24-70`).
**Description:** The AES-256-GCM layer exists but is opt-in and off in every shipped config. Frames/clips traverse `runCam→mofin→terp→coordinator` as plaintext and are stored unencrypted. Even when enabled, there is no protection against replay.
**Impact:** On-path sniffers can reconstruct surveillance footage; anyone with storage read access reads plaintext footage; clips can be modified in transit without detection (GCM only protects when the passphrase is actually configured end-to-end).
**Recommendation:** Require encryption to be on (fail-closed), propagate the passphrase via environment/secret store rather than CLI flags, or use mutually-authenticated TLS. Add per-clip anti-replay (e.g., monotonic nonce/counter, unique clip nonce).
---
### 9. HIGH — Live-feed poisoning via spoofed `SourceData` on TCP ingest
**Location:** `livestream-cache/main.go:299-321` (`handleTCPConnection`). `passphrase := cameraPassphrases[sourceData]`; with an empty passphrase the frame is accepted and stored under the spoofed key, overwriting a real camera's cached frames.
**Description:** The ingest server trusts `clip.SourceData` from the wire. Because there is no sender authentication (#1) and encryption is off (#8), an attacker connected to `:8085` can impersonate any configured camera and overwrite what viewers see on `/stream/...`.
**Impact:** Viewers are shown attacker-chosen images (denial of the real feed, social engineering, hiding the attacker's presence from the livestream while clips still may record differently).
**Recommendation:** Authenticate senders and validate `SourceData` against the configured camera list; reject unknown sources and never allow overwriting another camera's cache entry except from authenticated senders.
---
### 10. MEDIUM — Secrets in plaintext / visible in process listings
**Location:**
- Passphrases and DB password passed as CLI flags: `runCam/main.go:39`, `coordinator/main.go:43,47` (`-passphrase`, `-db-password`), `mofin/main.go:33`, `terp/main.go:31`. Visible to any local user via `ps`.
- MySQL DSN is built inline (`coordinator/main.go:56-57`) and defaults to `root` with an empty password (`coordinator/main.go:46-48`).
- Config files store passphrases in plaintext YAML (`config.example.yaml:11,15` ships `passphrase: "change-me"` — a universally-known key if left in place).
**Impact:** Local privilege escalation/privacy loss on the host; a "well-known" default key if example config is lifted wholesale; database exposed if `root`/empty password ever connects to a reachable endpoint.
**Recommendation:** Pass secrets via environment variables or a secrets file with `0600` perms; stop defaulting to `root`/empty; never ship a default key.
---
### 11. MEDIUM — Compiled binaries committed to the repo
**Location:** `livestream-cache/livestream-cache`, `terp/terp`, `mofin/mofin` (tracked; `terp/terp` additionally shows uncommitted modifications in the working tree). The `.gitignore` lists them (`runCam/runCam`, etc.) but they were added anyway.
**Description:** Binaries cannot be audited or attributed; a modified, untracked `terp/terp` means the deployed binary may not match the source.
**Impact:** Supply-chain / integrity risk if such a repo is shared — someone could execute a trojaned binary with full pipeline privileges and never notice.
**Recommendation:** `git rm --cached` all compiled artifacts, keep `.gitignore` enforced (use `git check-ignore`/pre-commit), and always build from source (`run-stack.sh` / `oko-run` already build-if-missing).
---
### 12. MEDIUM — Live-stream endpoint is a CPU DoS by design
**Location:** `livestream-cache/main.go:377-420`.
**Description:** Every connected client triggers a full `frameToImage` + JPEG `encodeJPEG` every 100 ms regardless of whether the underlying frame changed (cache re-encode is per client, not per-frame). There is no client limit or rate limit.
**Impact:** A modest number of unauthenticated clients saturates CPU and starves the feed for everyone; no auth means trivially scriptable by an attacker.
**Recommendation:** Encode once per new frame and share the JPEG across clients (broadcast model), cap concurrent stream consumers globally and per camera, and add per-IP rate limits.
---
### 13. MEDIUM — No slow-loris / timeouts protection (all TCP services)
**Location:** accept handlers in `coordinator/main.go:98-113`, `mofin/main.go:52-73`, `terp/main.go:59-102`, `livestream-cache/main.go:269-279`; no `SetReadDeadline`/`SetWriteDeadline` anywhere; `http.Server` without `ReadHeaderTimeout` (`livestream-cache/main.go:332-336`).
**Impact:** Idle connections can be held open indefinitely by any local network peer, exhausting file descriptors and goroutines.
**Recommendation:** Set connection deadlines (e.g., 10-30 s idle timeout) and server timeouts; consider a small max-connections gate.
---
### 14. MEDIUM — World-readable storage and logs
**Location:**
- `coordinator`: `MkdirAll(..., 0755)` dirs and clip JSON written `0644` (`coordinator/main.go:196,316`).
- `livestream.log` (955 KB) committed to the repo, containing operational details of the livestream backend.
**Impact:** Any local account can read surveillance footage and logs; perms + repo history mean footage persists beyond the operator's control.
**Recommendation:** Use restrictive perms (e.g., `0700` dirs, `0600` files), and keep logs out of git (`.gitignore` `logs/`).
---
### 15. LOW — Camera enumeration & metadata leakage in responses
**Location:** `livestream-cache/main.go:361-364` (404 vs 200 reveals configured camera IDs); stream/client IPs logged (`main.go:380`).
**Impact:** Enumerating which cameras exist over an unauthenticated endpoint; minor privacy exposure of viewer IPs in logs.
**Recommendation:** Return a uniform response for unknown vs known cameras (with auth in place), and consider not logging client IPs (or logging to an authenticated-only log).
---
### 16. LOW — `mofin -threshold` is truncated through `uint8`
**Location:** `mofin/main.go:32,111` — flag is `int` (default 30) but `processClip` casts to `uint8`; shipped configs use `threshold: 5000` (`config.yaml`, `run-stack.sh:76`, `bots_readme.md`), which silently becomes `5000 & 0xFF = 136`.
**Impact:** Not a security break, but indicates config values are misinterpreted; behavior is not what operators think, which can cause unexpected motion-gating results.
**Recommendation:** Clamp or validate threshold to 0-255, or document it as percent-based math.
---
### 17. LOW — Replay of previously captured clips
**Location:** `frame/frame.go:172-237` (AES-GCM without an anti-replay counter).
**Description:** GCM protects integrity/confidentiality but the protocol has no nonce-uniqueness enforcement across clips or sequence numbers, so a captured encrypted clip can be replayed into storage/livestream.
**Impact/Recommendation:** Low for a home system; add a unique per-clip nonce/preimage (e.g., GUID-derived) and monotonic counter if replay resistance matters.
---
### 18. INFO — `genGuid` panics on entropy failure; `CfgData.PassHash` is a misnomer
**Location:** `frame/cameraFunctions.go:59-67` (`panic` on `crypto/rand` failure — turns a rare-but-recoverable condition into a crash), and `frame/cameraFunctions.go:12-15,37,55` (`PassHash` holds the raw passphrase bytes, not a hash; `SourceData` embeds it as `user--pass`).
**Impact/Recommendation:** Entropy panic is a crash-under-adversarial-CSPRNG issue. The `PassHash` naming invites future misuse (embedding real secrets in `SourceData`); rename and ensure the actual passphrase is never serialized into `SourceData` (the current `runCam` path no longer does this — keep it that way).
---
## Cross-cutting recommendations
1. **Zero-trust the wire:** authenticate every peer and message (token or client-cert TLS), set explicit bind interfaces, and make encryption non-optional (fail closed).
2. **Validate everything decoded from the network:** length invariants (`CheckLenCorrelations`), frame counts, dimensions, `SourceData` charset, and path containment.
3. **Cap resources:** connection limits, decode sizes, worker-pool bounds, `http.Server` timeouts.
4. **Purge the repo:** re-key TLS, remove footage from history (`git filter-repo`), drop compiled binaries and logs, harden `.gitignore` + pre-commit hooks (block `*key*`, `*.pem`, storage/log output).
5. **Secrets hygiene:** environment-based secrets, no default `root`/empty DB creds, no examples with real-looking keys.
## Repo hygiene summary (what's tracked in git that shouldn't be)
| Item | Risk |
|---|---|
| `livestream-cache/key.pem` (private key) | TLS compromise → MITM of live streams |
| `coordinator/storage/**/clip.mp4`, `clip.json` | Real surveillance footage in history |
| `livestream-cache/livestream.log` | Operational data |
| `livestream-cache/livestream-cache`, `terp/terp`, `mofin/mofin` | Unverifiable compiled binaries (one modified) |