Skip to content

Commit 07ea031

Browse files
dkamclaude
andcommitted
Remove hardcoded internal IP from production hosts allowlist
192.168.2.246 was redundant with the 192.168.0.0/16 regex already in the CLINCH_ALLOW_INTERNAL_IPS block, and baked a specific lab IP into the repo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 209c549 commit 07ea031

2 files changed

Lines changed: 110 additions & 3 deletions

File tree

SECURITY_REVIEW_TODO.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Security Review — Tracking
2+
3+
Status of findings from the multi-surface security review (OIDC/OAuth2, ForwardAuth,
4+
WebAuthn/TOTP, sessions, admin/config). Work landed on branch
5+
`security/forward-auth-and-consent-csrf`.
6+
7+
## ✅ Done (branch `security/forward-auth-and-consent-csrf`)
8+
9+
All HIGH findings are closed. Each fix has tests; suite is green.
10+
11+
| Commit | Fix | Sev |
12+
|--------|-----|-----|
13+
| `703d24e` | ForwardAuth fail-open when no host header; consent endpoint CSRF | HIGH ×2 |
14+
| `8a095e4` | Bearer API-key skipped group check at use-time | HIGH |
15+
| `96a657e` | Open redirect via unvalidated `X-Forwarded-Host` in login redirect | HIGH |
16+
| `84ed462` | `CLINCH_HOST` made mandatory in deployed envs; dropped request-host fallback | MEDIUM |
17+
| `f38ac2e` | TOTP code replay within drift window (+ latent plaintext backup-code bug) | HIGH |
18+
| `406a79d` | SSRF via `backchannel_logout_uri` (metadata/loopback/RFC1918) | HIGH |
19+
| `57d7d1f` | Host-auth regex unanchored (`evil-example.com` matched) | HIGH |
20+
| `89bd5f1` | Disabled user could complete 2FA mid-flow / keep session; enforce active status | HIGH |
21+
| `cd862c7` | TOTP/backup/OAuth/PKCE `code` params not filtered from logs | MEDIUM |
22+
| `2426687` | `revoke_family!` didn't revoke access tokens on refresh-token reuse | HIGH |
23+
| `44892e3` | WebAuthn clone detection logged but didn't block; false-positive on synced passkeys | HIGH |
24+
| `d49e7ce` | CSP `unsafe-inline` removed (script-src + style-src → nonces) | HIGH |
25+
26+
**Verified false positive (no change):** PKCE *is* required by default —
27+
`require_pkce` column defaults to `true` (`db/schema.rb`), token endpoint enforces
28+
it, admin UI exposes the opt-out. Operational check: confirm no legacy confidential
29+
apps sit on `require_pkce = false`.
30+
31+
**Follow-up before relying on CSP change:** do one manual browser pass (DevTools
32+
console) on `/signin`, OAuth consent, a Turbo navigation, dark-mode toggle, and a
33+
WebAuthn sign-in — expect zero CSP violations. Dev is report-only so violations
34+
surface as warnings without breaking. Fallback if style-src surprises: keep
35+
`style-src 'unsafe-inline'`, ship script-src only.
36+
37+
## ☐ Remaining — MEDIUM
38+
39+
- [ ] **`id_token_hint` ignored at OIDC logout** — any client can redirect logout to
40+
any other registered client's post-logout URI. Validate the hint's `aud` and
41+
scope the redirect to that app. `app/controllers/oidc_controller.rb` (logout).
42+
- [ ] **`offline_access` doesn't gate refresh-token issuance** — refresh tokens are
43+
minted unconditionally; gate on the granted scope.
44+
`app/controllers/oidc_controller.rb` (authorization_code grant, ~line 564).
45+
- [ ] **CSP-report endpoint hardening** — unauthenticated, no rate limit / body-size
46+
cap, logs raw CRLF (log injection). Sanitize values, cap size, rate-limit.
47+
`app/controllers/api/csp_controller.rb`.
48+
- [ ] **Port not stripped from `X-Forwarded-Host`** in main verify + bearer paths →
49+
403 outages on non-standard ports (also a correctness bug). Reuse the
50+
port-stripping done in `check_forward_auth_token`.
51+
`app/controllers/api/forward_auth_controller.rb`.
52+
- [ ] **WebAuthn `acr:"2"` without enforced user verification** — `user_verification:
53+
"preferred"` lets a PIN-less key authenticate yet reports verified 2FA. Use
54+
`"required"`, or downgrade `acr` to `"1"` when the UV flag is absent.
55+
`app/controllers/sessions_controller.rb` (webauthn_challenge/verify),
56+
`app/controllers/webauthn_controller.rb`.
57+
- [ ] **`RESERVED_CLAIMS` incomplete** — missing `at_hash`/`auth_time`/`acr`; and
58+
`ApplicationUserClaims` has no reserved-name validation (User/Group do). Could
59+
let a custom claim overwrite a security claim. `app/services/oidc_jwt_service.rb`,
60+
`app/models/application_user_claim.rb`.
61+
- [ ] **`reset_session` not called on login** — defensive best practice for an IdP;
62+
clears pre-auth session state. `app/controllers/concerns/authentication.rb`
63+
(`start_new_session_for`).
64+
- [x] **Hardcoded private IP `192.168.2.246`** in `config/environments/production.rb`
65+
— removed; it was redundant with the `192.168.0.0/16` regex already in the
66+
`CLINCH_ALLOW_INTERNAL_IPS` block.
67+
- [ ] **CSP `form-action` widened by unvalidated `redirect_uri`** before auth — only
68+
add to `form-action` if the client_id+redirect_uri is a registered pair.
69+
`app/controllers/concerns/authentication.rb` (`allow_oauth_redirect_in_csp`).
70+
- [ ] **SVG `style` attribute permits `url()`/`expression()`** — mitigated today by
71+
`Content-Disposition: attachment`, but fragile. Sanitize CSS values or drop
72+
`style` from the allowlist. `app/models/svg_scrubber.rb`.
73+
- [ ] **WebAuthn error messages leak internals** — return generic errors to client,
74+
log detail server-side. `app/controllers/sessions_controller.rb`,
75+
`app/controllers/webauthn_controller.rb`.
76+
- [ ] **Account enumeration via webauthn challenge** — distinguishes "user not found"
77+
vs "no passkey". Return a uniform message. `app/controllers/sessions_controller.rb`
78+
(`webauthn_challenge`).
79+
- [ ] **`token_family_id` only 31 bits** (`SecureRandom.random_number(2**31)`) —
80+
birthday collision ~46k; use a UUID/string. `app/models/oidc_refresh_token.rb`.
81+
- [ ] **Session cookie uses sequential integer DB id** — HMAC-signed so not forgeable,
82+
but consider a random `token` column (Rails 8 generator default).
83+
`app/models/session.rb`, `app/controllers/concerns/authentication.rb`.
84+
- [ ] **Login rate-limit is IP-only** — no account lockout (distributed brute force /
85+
credential stuffing). Add failed-count + `locked_until` on users.
86+
- [ ] **Backup-code rate limit not reset on success** and is cache-based (resets on
87+
cache flush). Reset on success; consider DB-backed counter. `app/models/user.rb`.
88+
89+
## ☐ Remaining — LOW / INFO
90+
91+
- [ ] Public clients can't revoke their own tokens (revoke endpoint requires secret).
92+
- [ ] Basic-auth client creds not URL-decoded per RFC 6749 §2.3.1.
93+
- [ ] `token_hmac` columns nullable at DB level despite model `presence: true`.
94+
- [ ] Group names allow commas → injection into `X-Remote-Groups` (false memberships
95+
downstream). Add a format validator. `app/models/group.rb`.
96+
- [ ] `fa_token` leaks in redirect URL / Referer / history (60s TTL, host-bound).
97+
- [ ] Admin `domain_pattern` allows ReDoS — add a format validator.
98+
`app/models/application.rb`.
99+
- [ ] Forced-TOTP-setup login path can redirect-loop (`totp_required` + no TOTP).
100+
- [ ] `complete_setup` creates an unprompted session for any authenticated user.
101+
- [ ] Password min length only 8 — consider 12 + a max (bcrypt 72-byte truncation).
102+
- [ ] `support_unencrypted_data: true` left enabled (TOTP secret encryption migration).
103+
`config/initializers/active_record_encryption.rb`.
104+
- [ ] All crypto keys derived from a single `SECRET_KEY_BASE` root — document setting
105+
independent `ACTIVE_RECORD_ENCRYPTION_*` keys in production.
106+
- [ ] Log injection via user `email_address` in ForwardAuth logs (strip CRLF / use
107+
structured logging). `app/controllers/api/forward_auth_controller.rb`.
108+
- [ ] WebAuthn RP ID is the registrable domain (cross-subdomain credential roaming) —
109+
set `CLINCH_RP_ID` to the exact host unless roaming is intended.
110+
`config/initializers/webauthn.rb`.

config/environments/production.rb

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,6 @@ def self.ensure_https(url)
139139

140140
# Allow internal IP access for cross-compose or host networking
141141
if ENV["CLINCH_ALLOW_INTERNAL_IPS"] == "true"
142-
# Specific host IP
143-
allowed_hosts << "192.168.2.246"
144-
145142
# Private IP ranges for internal network access
146143
allowed_hosts += [
147144
/192\.168\.\d+\.\d+/, # 192.168.0.0/16 private network

0 commit comments

Comments
 (0)