Skip to content

chore(security): full-codebase security audit — hardening sweep#610

Merged
SoundMindsAI merged 5 commits into
mainfrom
chore_security_audit_hardening_2026_07
Jul 11, 2026
Merged

chore(security): full-codebase security audit — hardening sweep#610
SoundMindsAI merged 5 commits into
mainfrom
chore_security_audit_hardening_2026_07

Conversation

@SoundMindsAI

@SoundMindsAI SoundMindsAI commented Jul 10, 2026

Copy link
Copy Markdown
Owner

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

# Sev Area Fix
1 Med Frontend Clickjacking / security headers — the no-auth localhost control plane (Open PR, launch study, demo reset buttons) shipped zero HTTP security headers. Added X-Frame-Options: DENY, CSP frame-ancestors 'none', X-Content-Type-Options: nosniff, Referrer-Policy in next.config.mjs.
2 Med CI Unverified gitleaks binary — the secrets-scan job downloaded the gitleaks release asset with no checksum (release assets are mutable even under a fixed tag). Added a sha256sum --check gate against the upstream-published digest before extract/exec.
3 High/Med Deps Dependabot alerts — bumped undici (high, WS DoS → ≥7.28.0) and js-yaml (med, quadratic DoS → ≥4.2.0) via pnpm.overrides. Both dev-only transitive (jsdom/vitest, eslint); cleared both open alerts.
4 Low Backend Log scrubber coverage — the global structlog redactor covered only GitHub PATs. Extended it to OpenAI keys (sk-*, incl. sk-proj-/sk-svcacct-) as the CLAUDE.md Rule #10 defense-in-depth backstop. New [REDACTED-OPENAI-KEY] placeholder + unit tests.
5 Low Frontend javascript: link bypass — the guide markdown link resolver supplied a custom urlTransform, which disables react-markdown's built-in scheme allowlist. Chained defaultUrlTransform so javascript:/data: links render inert.
6 Low Frontend Unvalidated pr_url href — only render proposal.pr_url as a live <a href> when it is https://.
7 Low Scripts Corp-CA trust-on-first-usecorp-ca-extract.sh silently trusted whatever CA was MITM-ing the connection. Now prints the CA SHA256 fingerprint and requires explicit confirmation (--yes / RELYLOOP_CORP_CA_ASSUME_YES=1 for automation; refuses on a non-interactive shell) before installing it.
8 Low Backend/Agent SSE worker pinning — bounded 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 hold a streaming worker for the SDK's 600s default across the tool loop.
9 Low CI One unpinned action — SHA-pinned the sole tag-pinned action (actions/checkout@v4 in the reconcile workflow, to the repo's existing v4 SHA) and routed its dry_run input through env: 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 no X-GitHub-Delivery replay cache; current handlers are idempotent via the DB state machine so it's defense-in-depth only.

Reviewed and accepted (no change)

  • SSRF relyloop_allow_private_clusters=True default — by-design for MVP1 (local dev registers ES at 127.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 in bug_cluster_url_ssrf_hostname_bypass/phase2_idea.md.
  • CORS allow_credentials=True — explicit localhost origin allowlist (no wildcard); all /api/v1 endpoints consume application/json, forcing a preflight a non-allowlisted origin fails. No credentials exist to steal (no auth).
  • Bandit medium findings — all 3 are in scripts/seed_meaningful_demos.py, a local dev-only seeding CLI with closed-set (SCENARIOS) inputs and SQL escaping; not an API-reachable surface.
  • Remaining pnpm audit items — a Windows-only vite dev-server advisory and a dev-only @babel/core sourcemap read; both dev-tooling transitive, not Dependabot-flagged.

Testing

  • Backend: 2817 unit tests pass; ruff + mypy --strict clean on changed files; new redaction tests cover OpenAI-key scrubbing + near-miss non-matches.
  • Frontend: 1300 vitest pass (incl. new javascript:-sanitization and non-https-pr_url regression tests); tsc + eslint clean; next.config headers() validated.
  • corp-ca-extract.sh syntax-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:

  1. 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 on main HEAD (e75f1e19) for the same reason. This PR adds actions: read to match GitHub's recommended CodeQL permission template, but the repo-setting must be enabled out-of-band. Operator action required.

  2. backend (contract + integration + cov) — one integration test fails: test_synthetic_ubi_seed_round_trip_hits_rung_3 (seeds 640 UBI events into Elasticsearch, reads back rung_1 instead of rung_3). Code-stable ~6 weeks; untouched by this branch's diff; not a refresh race (the writer already uses refresh=wait_for). Root cause is a UBI-classifier/query-filter or engine-reachability read issue that needs a live ES stack to diagnose. Captured as 02_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).

SoundMindsAI and others added 2 commits July 10, 2026 17:56
…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>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread backend/app/domain/git/redaction.py Outdated
# 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,}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
_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>
@SoundMindsAI
SoundMindsAI force-pushed the chore_security_audit_hardening_2026_07 branch from 6add98f to 9002f8e Compare July 10, 2026 22:17
SoundMindsAI and others added 2 commits July 10, 2026 18:30
… 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>
@SoundMindsAI

Copy link
Copy Markdown
Owner Author

Review adjudication (pre-merge)

Gemini Code Assist — 1 finding, adjudicated:

Finding Severity Verdict Action
redaction.py:47sk-[A-Za-z0-9_-]{20,} false-positives on hyphenated words containing sk- (e.g. risk-assessment-questionnaire) Medium Accept — correct; - is in the class with no leading boundary, so the pattern matches mid-word Fixed in 5d8e8dfa: added \b anchor + regression test for both cited examples. A real key always begins at a token boundary, so true-positive coverage is unchanged.

Pre-existing red checks (NOT introduced by this PR — see PR description):

  • CodeQL — analysis runs; SARIF upload blocked by "Code scanning is not enabled for this repository" (repo-admin setting; red on main too).
  • backend (contract + integration) — one pre-existing integration test (test_synthetic_ubi_seed_round_trip_hits_rung_3, UBI subsystem, untouched by this diff, needs a live ES stack to diagnose). Captured as bug_ubi_fast_test_classifies_rung_1_in_ci.

All checks this PR's changes are responsible for are green. Merge-skew: clean (main unchanged at e75f1e19 since branch point).

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.

1 participant