chore(security): full-codebase security audit — hardening sweep#610
Conversation
…and scripts Full-codebase security audit sweep. No critical/high vulnerabilities were found; these are the safely-fixable low/medium residual-risk items. Fixes applied inline: - redaction: extend the global structlog scrubber to also redact OpenAI keys (sk-*, incl. sk-proj-/sk-svcacct-) as defense-in-depth, not just GitHub PATs (CLAUDE.md Rule #10 backstop). New [REDACTED-OPENAI-KEY] placeholder + unit tests. - ui: add baseline security headers (X-Frame-Options DENY, CSP frame-ancestors 'none', X-Content-Type-Options nosniff, Referrer-Policy) — clickjacking protection for the no-auth localhost control plane. - ui: chain react-markdown's defaultUrlTransform in the guide link resolver so a javascript:/data: link in a doc renders inert (a custom urlTransform otherwise disables the built-in scheme allowlist). - ui: only render proposal.pr_url as a live <a href> when it is https://. - ci: checksum-pin the gitleaks CI binary download (release assets are mutable even under a fixed tag); SHA-pin the one unpinned action (actions/checkout@v4) and route the reconcile dry_run input through env. - scripts: corp-ca-extract now prints the detected CA SHA256 fingerprint and requires an explicit trust confirmation (--yes / RELYLOOP_CORP_CA_ASSUME_YES=1 for automation; refuses on a non-interactive shell) before installing it — closes the TOFU gap. - agent: bound the chat orchestrator's AsyncOpenAI client with an operator-tunable per-request timeout (OPENAI_CHAT_HTTP_TIMEOUT_S, default 120s) so a slow/hostile OPENAI_BASE_URL can't pin a worker. Deferred (need product/UX decision or coordinated Dependabot work), captured as idea files: - bug_agent_confirmation_binds_tool_name_not_args (02_mvp2) - chore_supply_chain_pin_precommit_and_compose_images (04_ga) - bug_github_webhook_no_delivery_id_replay_cache (04_ga) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai>
…abot alerts Resolves the two open Dependabot alerts on the UI lockfile (both transitive, dev-only — undici via jsdom/vitest, js-yaml via eslint): - undici (high): WebSocket client DoS via fragment count bypass → >=7.28.0 - js-yaml (medium): quadratic-complexity DoS in merge-key handling → >=4.2.0 Both are dev/test tooling only — neither ships in the production Next.js bundle — but the override bump is cheap and clears the alerts. Ranges are scoped to the vulnerable major line so no incompatible copy is clobbered. Mirrors the existing postcss override pattern. Full test suite (1300 vitest), tsc, and lint remain green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai>
There was a problem hiding this comment.
Code Review
This pull request introduces several security hardening measures across the codebase, including configuring a timeout for the OpenAI chat client, redacting OpenAI API keys in logs, adding a trust-on-first-use gate to the corporate CA extraction script, setting baseline security headers in the UI, upgrading vulnerable frontend dependencies, and sanitizing links in Markdown guides and PR panels. Feedback on these changes suggests adding a word boundary prefix (\b) to the OpenAI key redaction regular expression to prevent false-positive redactions on common hyphenated words.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| # key alphabet (also matches the ``sk-proj-``/``sk-svcacct-`` prefixed | ||
| # forms since ``-`` is in the class). The 20-char floor keeps ordinary | ||
| # prose containing ``sk-`` from matching. | ||
| _OPENAI_KEY_PATTERN = re.compile(r"sk-[A-Za-z0-9_-]{20,}") |
There was a problem hiding this comment.
The regular expression sk-[A-Za-z0-9_-]{20,} can lead to false-positive redactions on common hyphenated words or phrases containing sk- if the subsequent part is 20 or more characters long. For example, phrases like risk-assessment-questionnaire (containing sk-assessment-questionnaire) or task-scheduling-service-impl (containing sk-scheduling-service-impl) will be incorrectly redacted to ri[REDACTED-OPENAI-KEY] or ta[REDACTED-OPENAI-KEY].\n\nTo prevent this, add a word boundary \b prefix to the pattern so that it only matches when sk- starts a word.
| _OPENAI_KEY_PATTERN = re.compile(r"sk-[A-Za-z0-9_-]{20,}") | |
| _OPENAI_KEY_PATTERN = re.compile(r"\bsk-[A-Za-z0-9_-]{20,}") |
Add actions:read to the CodeQL analyze job — GitHub's recommended permission set for the analyze action, which reads its own workflow run via the Actions API. NOTE: this does NOT by itself make CodeQL green. The analysis runs and exports SARIF fine, but the upload fails with "Code scanning is not enabled for this repository" — a repo-admin setting (Settings → Code security → Code scanning) that must be enabled out-of-band. CodeQL has been red on main (e75f1e1) for the same reason; that's an operator handoff, not a code fix. Documented in the PR body. Tangential — noticed during the 2026-07-10 security audit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai>
6add98f to
9002f8e
Compare
… idea file The pr.yml backend integration job fails on test_synthetic_ubi_seed_round_trip_hits_rung_3 (seeds 640 ES events, reads back rung_1 not rung_3). Code-stable ~6 weeks, untouched by this branch's diff; not a refresh race (writer uses refresh=wait_for). Needs a live ES stack to distinguish a classifier/query-filter bug from an engine-reachability read failure, so captured rather than force-fixed in this security PR. Tangential — surfaced during the 2026-07-10 security audit (PR #610) CI watch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai>
…review)
The sk-[A-Za-z0-9_-]{20,} pattern (with '-' in the class) false-positived on
hyphenated words containing 'sk-' + 20 chars, e.g. 'risk-assessment-
questionnaire' -> 'ri[REDACTED-OPENAI-KEY]'. Add a leading \b so 'sk' must
start a word; a real key always begins at a token boundary, so true-positive
coverage is unchanged. Regression test added for the two Gemini examples.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai>
Review adjudication (pre-merge)Gemini Code Assist — 1 finding, adjudicated:
Pre-existing red checks (NOT introduced by this PR — see PR description):
All checks this PR's changes are responsible for are green. Merge-skew: clean ( |
Summary
A full-codebase security audit (backend API/services/agent/workers, frontend, CI/CD + container/infra, secrets + LLM surfaces) plus automated scanners (Bandit, pip-audit, pnpm audit, gitleaks, tracked-file secret scan). No critical or high-severity vulnerabilities were found — the codebase has an unusually strong posture (SandboxedEnvironment for all templates, constant-time HMAC webhook verification, git tokens injected via process env not argv, path-traversal containment checks, SHA-pinned actions, least-privilege CI tokens, non-root digest-pinned containers, loopback-only port binds, file-mounted secrets).
This PR fixes the safely-fixable low/medium residual-risk items inline and captures the design-level items (that need a product/UX decision or coordinated Dependabot work) as idea files. Because a security audit is inherently cross-cutting, this PR intentionally spans backend, frontend, CI, and scripts.
Fixes applied
X-Frame-Options: DENY, CSPframe-ancestors 'none',X-Content-Type-Options: nosniff,Referrer-Policyinnext.config.mjs.sha256sum --checkgate against the upstream-published digest before extract/exec.undici(high, WS DoS → ≥7.28.0) andjs-yaml(med, quadratic DoS → ≥4.2.0) viapnpm.overrides. Both dev-only transitive (jsdom/vitest, eslint); cleared both open alerts.sk-*, incl.sk-proj-/sk-svcacct-) as the CLAUDE.md Rule #10 defense-in-depth backstop. New[REDACTED-OPENAI-KEY]placeholder + unit tests.javascript:link bypass — the guide markdown link resolver supplied a customurlTransform, which disables react-markdown's built-in scheme allowlist. ChaineddefaultUrlTransformsojavascript:/data:links render inert.pr_urlhref — only renderproposal.pr_urlas a live<a href>when it ishttps://.corp-ca-extract.shsilently trusted whatever CA was MITM-ing the connection. Now prints the CA SHA256 fingerprint and requires explicit confirmation (--yes/RELYLOOP_CORP_CA_ASSUME_YES=1for automation; refuses on a non-interactive shell) before installing it.AsyncOpenAIclient with an operator-tunable per-request timeout (OPENAI_CHAT_HTTP_TIMEOUT_S, default 120s) so a slow/hostileOPENAI_BASE_URLcan't hold a streaming worker for the SDK's 600s default across the tool loop.actions/checkout@v4in the reconcile workflow, to the repo's existing v4 SHA) and routed itsdry_runinput throughenv:for consistency with the dco.yml convention.Deferred (captured as idea files)
These need a product/UX decision or coordinated Dependabot work, so per the CLAUDE.md inline-vs-idea rubric they're filed rather than forced into this PR:
02_mvp2/bug_agent_confirmation_binds_tool_name_not_args— the chat confirmation gate binds the tool name (recently hardened) but not the tool arguments, so a confirmed mutation could theoretically fire against a different resource ID than the human approved. Needs a UX decision on how the assistant echoes/matches the target.04_ga/chore_supply_chain_pin_precommit_and_compose_images— pre-commit hooks and Compose service images are tag-pinned, not digest-pinned (inconsistent with the SHA-pinned actions/Dockerfiles). Wants a coordinated Dependabot change.04_ga/bug_github_webhook_no_delivery_id_replay_cache— the webhook has noX-GitHub-Deliveryreplay cache; current handlers are idempotent via the DB state machine so it's defense-in-depth only.Reviewed and accepted (no change)
relyloop_allow_private_clusters=Truedefault — by-design for MVP1 (local dev registers ES at127.0.0.1:9200); flipping it would break the core local-dev flow. Already acknowledged in spec §10 and slated to flip at MVP3. The DNS-rebinding TOCTOU follow-on is already tracked inbug_cluster_url_ssrf_hostname_bypass/phase2_idea.md.allow_credentials=True— explicit localhost origin allowlist (no wildcard); all/api/v1endpoints consumeapplication/json, forcing a preflight a non-allowlisted origin fails. No credentials exist to steal (no auth).scripts/seed_meaningful_demos.py, a local dev-only seeding CLI with closed-set (SCENARIOS) inputs and SQL escaping; not an API-reachable surface.pnpm audititems — a Windows-only vite dev-server advisory and a dev-only@babel/coresourcemap read; both dev-tooling transitive, not Dependabot-flagged.Testing
ruff+mypy --strictclean on changed files; new redaction tests cover OpenAI-key scrubbing + near-miss non-matches.javascript:-sanitization and non-https-pr_urlregression tests);tsc+eslintclean;next.configheaders()validated.corp-ca-extract.shsyntax-checked (bash -n).🤖 Generated with Claude Code
CI status — two RED checks, both pre-existing and NOT caused by this PR
Verified during the CI watch; documented here so the red checks aren't mistaken for regressions from this sweep:
CodeQL (
Analyze) — the analysis runs and exports SARIF fine, but the upload fails with "Code scanning is not enabled for this repository." This is a repo-admin setting (Settings → Code security → Code scanning), not a code fix. Red onmainHEAD (e75f1e19) for the same reason. This PR addsactions: readto match GitHub's recommended CodeQL permission template, but the repo-setting must be enabled out-of-band. Operator action required.backend (contract + integration + cov)— one integration test fails:test_synthetic_ubi_seed_round_trip_hits_rung_3(seeds 640 UBI events into Elasticsearch, reads backrung_1instead ofrung_3). Code-stable ~6 weeks; untouched by this branch's diff; not a refresh race (the writer already usesrefresh=wait_for). Root cause is a UBI-classifier/query-filter or engine-reachability read issue that needs a live ES stack to diagnose. Captured as02_mvp2/bug_ubi_fast_test_classifies_rung_1_in_ci. All contract tests and all of this PR's own new tests pass; only this one pre-existing integration test is red.Every check that this PR's changes are actually responsible for is green (frontend static-checks/tests/build, backend unit + static-checks, docker builds, gitleaks, secrets-guard, DCO, all freshness gates).