Files

278 lines
19 KiB
Markdown
Raw Permalink Normal View History

2026-09-09 21:44:05 -05:00
# 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) |