chore(SFLW-51): allow loopback dev origins so dashboard WebSocket connects in dev mode#38
Conversation
…nects in dev mode
The /ws upgrade failed in dev mode (vite dev:dashboard) with a 500 handshake.
Root cause was NOT the localhost/IPv6 mismatch the issue hypothesized — both
localhost and 127.0.0.1 handshake fine directly against the backend. The 500
was @fastify/cors rejecting the upgrade: the allowlist only contained the
backend's own port plus a hardcoded Vite port (5173), so any other dev-server
port (the e2e smoke harness uses 5185) sent a cross-origin Origin that CORS
threw on ("Not allowed by CORS").
Fix: in non-production, getCorsConfig now accepts any loopback origin
regardless of port (new isLoopbackOrigin helper). The dashboard already binds
localhost-only, so this does not widen exposure beyond the local machine.
Production (same-origin, frontend served by the backend) is unchanged.
Also point the vite proxy targets at 127.0.0.1 to match the backend's IPv4
bind exactly, and remove the now-obsolete PRE_EXISTING_WS_PROXY_ERROR e2e
suppression so the smoke suite actively guards the fix.
Verified: security-utils unit tests (+6 new), full unit suite (342), and the
dashboard smoke e2e suite all pass with the suppression removed.
|
Warning Review limit reached
More reviews will be available in 1 hour, 58 minutes, and 19 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (3)
WalkthroughAdds an exported ChangesLoopback origin CORS and proxy fix
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Pull request overview
This PR fixes dashboard WebSocket failures in dev mode by relaxing CORS handling for local development origins (so Vite can run on non-default ports) and aligning Vite’s proxy target with the backend’s IPv4 loopback bind, while also tightening the e2e smoke harness to fail on any console error.
Changes:
- Add
isLoopbackOrigin()and updategetCorsConfig()to allow loopback origins on any port in non-production environments. - Update Vite dev proxy targets to use
127.0.0.1for/apiand/ws. - Remove the previously-whitelisted WebSocket proxy error filter so smoke tests actively enforce the fix.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/dashboard_frontend/vite.config.ts | Switch dev proxy targets to 127.0.0.1 for more reliable loopback routing. |
| src/core/security-utils.ts | Expand non-production CORS to permit loopback origins regardless of port via isLoopbackOrigin(). |
| src/core/tests/security-utils.test.ts | Add unit tests covering the new loopback-origin and updated CORS behavior. |
| e2e/helpers/dashboard-smoke-harness.ts | Remove the prior WS error suppression so smoke tests fail on any console error. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | ✅ 12 (≤ 100 complexity) |
| Duplication | ✅ 0 (≤ 5 duplication) |
AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
Pull Request Overview
The PR successfully addresses development-mode WebSocket connectivity issues by refining CORS loopback handling and standardizing Vite proxy targets to IPv4. Codacy analysis indicates the changes are up to standards, and all primary acceptance criteria have been addressed.
The primary concern that should be addressed before merging is a security flaw in the loopback origin validation. The current implementation uses a prefix check that allows hostnames like '127.attacker.com' to be treated as valid loopback origins. This exposes the application to cross-site risks in development environments. Improving the regex and simplifying the hostname extraction will resolve this risk.
Test suggestions
- Verify isLoopbackOrigin correctly identifies localhost, 127.0.0.1, and [::1] as valid loopback origins regardless of port.
- Verify isLoopbackOrigin rejects external IP addresses and malformed URLs.
- Verify getCorsConfig allows loopback origins when NODE_ENV is set to development.
- Verify getCorsConfig rejects loopback origins in production if they are not in the explicit allowlist.
- Verify the smoke test suite successfully detects console errors for WebSocket failures.
TIP Improve review quality by adding custom instructions
TIP How was this review? Give us feedback
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/core/security-utils.ts`:
- Around line 73-77: The isLocalhostAddress helper function is too permissive in
its validation and accepts any hostname starting with 127., creating a CORS
bypass vulnerability where hostnames like 127.attacker.com would be incorrectly
treated as loopback. Fix the isLocalhostAddress function to only accept exact
loopback addresses (127.0.0.1, localhost, ::1, and IPv6 loopback equivalents)
rather than any hostname with a 127. prefix, ensuring that hostnames containing
additional characters after 127. (such as 127.attacker.com) are properly
rejected.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: f5ac6ecf-86f5-4475-81c4-88773a7cc701
📒 Files selected for processing (4)
e2e/helpers/dashboard-smoke-harness.tssrc/core/__tests__/security-utils.test.tssrc/core/security-utils.tssrc/dashboard_frontend/vite.config.ts
…ot review)
isLoopbackOrigin delegated to isLocalhostAddress, whose loose
startsWith('127.') would also match hostnames like "127.example.com" —
a CORS-bypass in non-production. Replace with a strict IPv4 127.0.0.0/8
check (four numeric octets, first === 127, each 0-255); localhost and ::1
are still accepted. isLocalhostAddress is left unchanged (its bindAddress
use is a separate, pre-existing concern).
Also consolidate the duplicated getCorsConfig test describe into the
existing block and add a regression case asserting 127.example.com is
rejected. Unit suite 340 green; build clean.
…0.0.0/8
Codacy + CodeRabbit both flagged that isLocalhostAddress's loose
startsWith('127.') accepts hostnames like "127.example.com" — a hole in
both the CORS loopback path and the bindAddress-validation guard
(config.ts, multi-server.ts). Replace the prefix check with a strict IPv4
127.0.0.0/8 regex (each octet 0-255); "localhost" and "::1" still pass.
All legitimate bindAddress values (127.0.0.1, localhost, ::1) are
preserved, and existing 127.x.x.x test cases stay green.
isLoopbackOrigin now delegates to the hardened helper (single source of
truth), keeping the IPv6 bracket-strip — Node's URL.hostname returns
"[::1]" with brackets, so the strip is required, not redundant.
Added an isLocalhostAddress regression case for 127.example.com /
invalid-octet inputs. Unit suite 341 green; build clean.
…able proxy host (Copilot review) Addresses Copilot round-3 feedback: - getCorsConfig: the non-production loopback relaxation now applies only when the configured allowedOrigins already trusts loopback. A user who overrides allowedOrigins to exclude loopback (e.g. a shared dev/staging box with NODE_ENV!=='production') is respected instead of being silently widened open. - vite.config.ts: proxy target host is now configurable via VITE_DASHBOARD_HOST (default 127.0.0.1), so dev setups whose backend binds ::1/localhost can point the proxy accordingly. - tests: restore NODE_ENV via a describe-level afterEach that deletes the var when it was originally unset (assignment would set the literal "undefined" and leak into later tests). Added a regression test for the allowlist-gating. Unit suite 342 green; smoke e2e 2 green; build clean.
… gate The repetitive per-test boilerplate (build config → getCorsConfig → vi.fn() → origin()) across the getCorsConfig cases tripped Codacy's duplication metric (+6, threshold ≤5). Extract a checkOrigin(origin, allowedOrigins?) helper and collapse each case to a single assertion. Test-only; 342 unit tests still green.
SFLW-51 — dashboard WebSocket fails in dev mode (corrected root cause)
In dev mode (
npm run dev:dashboard), the/wsupgrade failed withUnexpected response code: 500and the connection indicator never reached Connected.Root cause — not what the issue hypothesized
The issue blamed a
localhost→ IPv6::1mismatch. Empirical probing disproved that: bothlocalhostand127.0.0.1handshake fine directly against the backend, with or without IPv6. The 500 body was:@fastify/corswas rejecting the WS upgrade GET.generateAllowedOrigins()builds the allowlist from the backend's own port plus a hardcoded Vite port5173. Any other dev-server port sends a cross-originOriginheader that CORS throws on. The e2e smoke harness runs Vite on 5185, so it always failed. Production is same-origin (frontend served from the backend process), so it never triggered — matching the "dev mode only" tag.Fix
src/core/security-utils.ts— in non-production,getCorsConfignow accepts any loopback origin regardless of port (newisLoopbackOriginhelper). The dashboard already binds localhost-only, so this does not widen exposure beyond the local machine. Production (same-origin) behavior is unchanged.src/dashboard_frontend/vite.config.ts— point the/api+/wsproxy targets at127.0.0.1to match the backend's IPv4 bind exactly (defensive; not the primary fix).e2e/helpers/dashboard-smoke-harness.ts— remove the now-obsoletePRE_EXISTING_WS_PROXY_ERRORsuppression so the smoke suite actively guards the fix.Verification
security-utilsunit tests + 6 new cases forisLoopbackOrigin/getCorsConfig— greentest:e2e:smoke) — 2 passed with the suppression removed and no console WS/500 errorsnpm run build— cleanNotes
/gsdchore; closes SFLW-51.Summary by CodeRabbit
Tests
Bug Fixes