Skip to content

security: harden CI workflows, auth, setup surface, and deps#305

Merged
htilly merged 1 commit into
masterfrom
security/harden-ci-auth-setup
Jul 14, 2026
Merged

security: harden CI workflows, auth, setup surface, and deps#305
htilly merged 1 commit into
masterfrom
security/harden-ci-auth-setup

Conversation

@htilly

@htilly htilly commented Jul 14, 2026

Copy link
Copy Markdown
Owner

Summary

Hardening from a security review of the repo. No product features removed; behavior changes are security-by-default and called out below.

🔴 Critical — GitHub Actions script injection (RCE)

  • Deleted the redundant enhance_feature_requests.yml. Its check-label job ran ISSUE_BODY="${{ github.event.issue.body }}" inside a run: shell with no if guard, so any user opening an issue could execute arbitrary code on the runner. The remaining feature-request-enhance.yml is a functional superset (adds workflow_dispatch, concurrency, and the AI codegen job), so nothing is lost.
  • feature-request-enhance.yml: all untrusted issue content is passed via env: and referenced as "$VAR" (never interpolated into run:); $GITHUB_OUTPUT heredocs use random delimiters (an issue body line EOF can no longer inject step outputs); each job now has least-privilege permissions: (only generate-implementation keeps contents/pull-requests: write).
  • confluence-changelog.yml: release body/fields moved to env:, body written with printf.
  • generate-implementation.mjs: ISSUE_NUMBER validated ^[0-9]+$ before it is embedded in execSync/patch file paths.

🟠 High

  • Dependencies: undici override → ^7.28.0, added form-data ^4.0.6 (both resolve High advisories; verified installed). Remaining audit highs are the previously-accepted ip/sonos SSRF documented in docs/SECURITY.md.
  • Setup surface: isLocalRequest now treats any forwarded request as non-local, closing the "behind a same-host reverse proxy every client looks local → setup bootstrap token bypassed" hole. saveConfig now allowlists known keys and drops adminPasswordHash, __proto__, and arbitrary injected fields.

🟡 Medium

  • Login rate-limit: getClientIp only trusts X-Forwarded-For/X-Real-IP when the new trustProxy config is true (default off), so a forged header can no longer bypass rate limiting. Falls back to the socket peer address.
  • WebAuthn: the unauthenticated /api/auth/webauthn/status endpoint no longer returns credential-file contents (only exists/size/credentialCount); a configured webauthnOrigin is no longer overwritten by request headers.
  • SSRF: validate-sonos rejects loopback, link-local (incl. 169.254.169.254 metadata), and multicast/reserved target IPs.

Config / tests

  • Added trustProxy to config.json.example and the admin config allowlist (boolean).
  • Updated 3 auth-handler tests that asserted the old, insecure X-Forwarded-For default to assert the new secure-by-default behavior.

⚠️ Deployment note

If you run SlackONOS behind a reverse proxy, set "trustProxy": true so rate limiting sees real client IPs (otherwise all requests share the proxy IP).

Deliberately NOT changed (would break functionality)

  • featurerequest left open to normal users — gating it to admin removes the feature's purpose (risk is only issue-spam, Low). Toggle in lib/command-registry.js if desired.
  • CSP unsafe-inline/unsafe-eval — admin/setup UI relies on inline scripts; removing needs a UI refactor.
  • Docker NET_ADMIN — required for Sonos SSDP discovery.

Testing

  • Changed-module suites pass: auth-handler, setup-bootstrap, http-utils, setup-wizard, command-registry, admin-api.
  • node --check clean on all changed JS/MJS; both edited workflows parse as valid YAML; package.json and config.json.example parse as valid JSON.

Git hygiene (verified, no change needed)

No real secrets are or ever were committed — config/config.json, keys/*.pem, and .env are gitignored and absent from history. The only .pem/.key in history are dummy "snakeoil" test fixtures from a historical node_modules commit.

🤖 Generated with Claude Code

Address findings from a security review of the repo.

Critical — GitHub Actions script injection (RCE):
- Delete redundant enhance_feature_requests.yml, whose unguarded check-label
  job interpolated github.event.issue.body into a run: shell on every opened
  issue (any user -> arbitrary code on the runner). The remaining
  feature-request-enhance.yml is a functional superset.
- feature-request-enhance.yml: pass all untrusted issue content via env vars
  (never spliced into run:), use random heredoc delimiters for $GITHUB_OUTPUT,
  and add least-privilege per-job permissions.
- confluence-changelog.yml: move release body/fields to env and write with
  printf instead of a quoted heredoc.
- generate-implementation.mjs: validate ISSUE_NUMBER (^[0-9]+$) before it is
  embedded in execSync/patch file paths.

High — dependencies:
- Bump undici override to ^7.28.0 and add form-data ^4.0.6 (both High advisories).

High — setup surface:
- isLocalRequest: treat any forwarded request as non-local so a same-host
  reverse proxy no longer makes every remote client look local and bypass the
  setup bootstrap token.
- saveConfig: allowlist known keys; drop adminPasswordHash, __proto__, and any
  other injected/unknown fields.

Medium:
- auth-handler getClientIp: only trust X-Forwarded-For / X-Real-IP when the new
  trustProxy config is enabled (default off), so a forged header can no longer
  bypass login rate limiting. Falls back to the socket peer address.
- webauthn-handler: the unauthenticated /status endpoint no longer returns
  credential-file contents (only exists/size/credentialCount); a configured
  webauthnOrigin is no longer overwritten by request headers.
- setup validate-sonos: reject loopback, link-local (incl. 169.254.169.254
  metadata), and multicast/reserved target IPs.

Config/tests:
- Add trustProxy to config.json.example and the admin allowlist (boolean).
- Update auth-handler tests that asserted the old (insecure) X-Forwarded-For
  default to assert the secure-by-default behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 14, 2026 12:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@htilly
htilly merged commit 55d5b76 into master Jul 14, 2026
5 checks passed
@htilly
htilly deleted the security/harden-ci-auth-setup branch July 14, 2026 12:49
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.

2 participants