Audit date: 2026-06-17 Scope: Phase 23 public GitHub release gate ASVS level: L1 (LAN appliance, single-user, no authentication)
| Severity | Finding | Status |
|---|---|---|
| BLOCKER | Identifiable face photo committed to git history | OPEN |
| BLOCKER | .claude/scheduled_tasks.lock committed — internal session state tracked in public repo |
OPEN |
| HIGH | web_host defaults to 0.0.0.0 with no auth; unauthenticated control plane if Pi is internet-facing |
ACCEPTED (LAN-only; see mitigations) |
| HIGH | Google Fonts loaded from CDN in control panel HTML — browser makes external request on every page load | OPEN |
| INFO | No CSRF protection on POST API routes | ACCEPTED (LAN-only; same reasoning as WR-06) |
| CLOSED | Feature flags (notifications/cloud/GPIO) OFF by default | CLOSED |
| CLOSED | No hardcoded secrets in src/, config/, scripts/ | CLOSED |
| CLOSED | .env gitignored; .env.example has no real values |
CLOSED |
| CLOSED | output/ gitignored; event recordings not tracked |
CLOSED |
| CLOSED | .venv/ gitignored |
CLOSED |
| CLOSED | models/*.tflite gitignored |
CLOSED |
| CLOSED | .claude/worktrees/ gitignored |
CLOSED |
| CLOSED | Input validation on all POST API routes (allowlist via VALID_MODES/VALID_PREPROCESS_MODES/VALID_HUD_THEMES; threshold clamped 0.05–0.95) | CLOSED |
| CLOSED | No shell injection sinks (subprocess call in preview/sink.py uses list argv, no shell=True) |
CLOSED |
| CLOSED | Flask debug mode not enabled (no debug=True in app.run call) |
CLOSED |
| CLOSED | No eval/exec on user-controlled input | CLOSED |
| CLOSED | output/ write path uses pathlib.Path.resolve() — no path traversal via config |
CLOSED |
File: .planning/phases/19-hand-fidelity/evidence-live-skeleton-offset.jpg
Evidence: 640×480 JPEG, tracked in git (confirmed via git ls-files), contains a clear full-face photograph of a real person (presumably the owner) with interior room background visible.
Why this is a blocker for public release: The image is in git history and the working tree. A git push to a public repo will publish it. There is no .gitattributes export-ignore rule and no .gitignore entry for .planning/. The photo is unambiguously identifiable and includes home-interior context.
Required action before public push:
- Remove the file from the working tree and all git history using
git filter-repo(orgit filter-branch):git filter-repo --path .planning/phases/19-hand-fidelity/evidence-live-skeleton-offset.jpg --invert-paths - Force-push the rewritten history to the remote (required after filter-repo; confirm before doing).
- Add
.planning/to.gitignoreif planning artifacts should not be public, or add*.jpg/*.jpeg/*.pngunder.planning/specifically.
Alternatively: exclude .planning/ from the public repo entirely by adding it to .gitignore before the first push, then running filter-repo to remove all currently tracked .planning/ content from history.
File: .claude/scheduled_tasks.lock
Content: {"sessionId":"b49ec618-...","pid":301444,"procStart":"540287","acquiredAt":1781686608166}
Evidence: Confirmed tracked via git ls-files.
Why this matters: This is a runtime lock file generated by the Claude Code agent runtime. It contains a session UUID, a PID, and a process start timestamp. It has no business being in a public repo. It is not covered by .gitignore (.claude/worktrees/ is excluded, but not .claude/scheduled_tasks.lock).
Required action before public push:
- Add
.claude/scheduled_tasks.lockto.gitignore. - Remove it from git tracking:
git rm --cached .claude/scheduled_tasks.lock - Commit that change.
The .claude/agents/ and .claude/skills/ files are intentional project artifacts and are fine to publish.
Files: src/ai_pi_cam/web/server.py (routes), src/ai_pi_cam/config/settings.py:104 (web_host default), config/config.yaml:19, config/examples/config.example.yaml:61–65
Detail: Five POST routes (/api/mode, /api/threshold, /api/preprocess, /api/theme, /api/state) accept unauthenticated requests. web_host defaults to 0.0.0.0, binding to all interfaces. Any host that can reach the Pi on port 8080 can change detection mode, thresholds, and HUD settings without authentication.
Threat model disposition: ACCEPTED for a LAN-only single-user appliance. The documented project constraint is "LAN-only web view." This is consistent with the design intent.
Condition for acceptance (must be in README / DEPLOYMENT.md before publish):
- A prominent security note warning that the web port must not be exposed to the internet.
- Explicit firewall guidance:
sudo ufw allow from 192.168.0.0/16 to any port 8080or equivalent. - Guidance to set
web_host: 127.0.0.1or a specific LAN IP inconfig/config.yamlif additional hardening is desired (this does not break LAN access — clients connect to the Pi's LAN IP, not 127.0.0.1 from outside).
Current state of documentation: config/examples/config.example.yaml:62 has a one-line comment ("Never expose to a public/untrusted IP without additional security") and docs/DEPLOYMENT.md mentions the feature-flags are safe but contains no firewall guidance and no explicit warning about the unauthenticated control plane. This gap must be filled before public release.
Input validation (confirmed CLOSED): All POST routes validate inputs against allowlists in AppState (VALID_MODES, VALID_PREPROCESS_MODES, VALID_HUD_THEMES) and return 400 on invalid input. Threshold is clamped to [0.05, 0.95] with type validation. No injection sinks.
File: src/ai_pi_cam/web/control_panel.html:7–9
Detail: The control panel loads two font families from fonts.googleapis.com and fonts.gstatic.com on every page open. This means every browser that views the panel makes outbound requests to Google's servers, which can log the client IP. For a hobbyist LAN appliance this is a minor operational concern, but for a public repo it establishes a privacy expectation mismatch: users who believe they are running a local-only appliance are inadvertently pinging Google on every session.
Severity: HIGH for a project whose stated constraint is "no cloud" and "LAN-only."
Required action before public push: Either:
- Self-host the fonts (download and serve as static files from Flask, or inline as base64 in the HTML), or
- Add a note to the README and config.example.yaml explaining the fonts CDN load and how to disable it (e.g. set
web.host: 127.0.0.1and use a browser with font-blocking, or replace the<link>tags with a local font stack).
The fallback font stacks already in the CSS (ui-monospace,monospace and sans-serif) are functional — removing the Google Fonts <link> tags degrades aesthetics only.
| Finding | Evidence Location |
|---|---|
| Feature flags OFF by default | settings.py:45–47 (features_notifications_enabled=False, features_cloud_enabled=False, features_gpio_enabled=False); all integration stubs guard with if not settings.features_*_enabled: return |
| notify.py raises NotImplementedError, no SMTP creds read | src/ai_pi_cam/integrations/notify.py:3–9 |
| cloud.py raises NotImplementedError, no token read | src/ai_pi_cam/integrations/cloud.py:3–9 |
| gpio.py raises NotImplementedError, no pin read | src/ai_pi_cam/integrations/gpio.py:3–9 |
| No hardcoded credentials | Full grep of src/, config/, scripts/ for password, token, api_key, secret, smtp, credential — no matches outside comments/stubs |
.env gitignored |
.gitignore:2 |
.env.example has no real values |
All credential lines are commented-out placeholders |
output/ gitignored |
.gitignore:19; confirmed empty in git ls-files output/ |
models/*.tflite gitignored |
.gitignore:22 |
.venv/ gitignored |
.gitignore:16 |
.claude/worktrees/ gitignored |
.gitignore:35 |
| API allowlist validation | state.py:10–11,65–67,75–77,82–84,93–104 |
| No shell=True subprocess | preview/sink.py:10–13 — subprocess.run(["xrandr"], ...) with list argv |
| Flask debug=False | server.py:109–114 — app.run(..., use_reloader=False) with no debug= arg; Flask defaults to debug=False |
| No eval/exec on user input | Full grep of src/ — zero matches |
| Output path resolved | events/recorder.py:14 — pathlib.Path(settings.output_dir).resolve() |
Before git push to a public remote:
- B-01 MUST: Remove
.planning/phases/19-hand-fidelity/evidence-live-skeleton-offset.jpgfrom git history and working tree (git filter-repo --invert-paths --path ...). Consider excluding all of.planning/from the public repo. - B-02 MUST: Add
.claude/scheduled_tasks.lockto.gitignore, rungit rm --cached, commit. - H-01 SHOULD: Add firewall/LAN-only warning with ufw example to
docs/DEPLOYMENT.mdandREADME.md. - H-02 SHOULD: Self-host fonts or document the CDN load as a known privacy trade-off in
README.md. - Verify
git ls-files | grep -E "\.(jpg|jpeg|png|mp4|h264)$"returns only intentional images (currently: one — the face photo, which must be removed). - Run
git ls-files | grep "\.env$"— must return empty.