You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Problem: Control UI origin checks rejected Electron origins like app://localhost even when explicitly listed in gateway.controlUi.allowedOrigins.
Why it matters: ClawControl desktop app connections over remote gateways (for example Tailscale Serve) were blocked unless users used wildcard "*" origins.
What changed: Origin parsing now normalizes custom-scheme URL origins with host components (for example app://localhost) instead of treating them as opaque null origins.
What changed: Added regression coverage proving allowedOrigins: ["app://localhost"] is accepted.
What did NOT change (scope boundary): No auth, pairing, host-header fallback, or wildcard origin behavior was changed.
This PR fixes a bug where custom protocol origins (e.g., app://localhost used by Electron/Tauri apps) could never be matched against the allowedOrigins allowlist in the gateway's origin check logic. The root cause is a WHATWG URL spec behavior: url.origin returns the string "null" for any non-special scheme (anything that isn't http, https, ws, wss, ftp, or file), so allowlist lookups using that value would always fail.
The fix in parseOrigin() adds a normalization step that reconstructs the origin as ${url.protocol}//${url.host} when url.origin === "null" but url.host is non-empty, producing the expected app://localhost string. The existing guard if (!trimmed || trimmed === "null") (line 14) continues to correctly reject raw null origin headers before any URL parsing occurs, so this change does not weaken the protection against opaque-origin bypass attempts. blob: and data: URLs are also unaffected because their url.host is empty, keeping them in the url.origin === "null" path unchanged.
src/gateway/origin-check.ts: adds a one-line normalization inside parseOrigin to recover a meaningful origin string for non-special-scheme URLs.
src/gateway/origin-check.test.ts: adds a regression test confirming that an app://localhost origin is accepted with matchedBy: "allowlist" when it appears in allowedOrigins.
Confidence Score: 5/5
This PR is safe to merge — it is a minimal, well-targeted fix with a direct regression test and no changes to security-critical logic paths.
The change is a single-line normalization in parseOrigin that only activates for URLs where url.origin === "null" AND url.host is non-empty. This is a narrow, well-understood edge case (non-special WHATWG URL schemes). The existing opaque-origin null guard is untouched, blob:/data: URLs are unaffected, and the new behavior (matching custom-protocol origins against the allowlist) is exactly the intended fix. The accompanying regression test validates both the success path and the matchedBy discriminant.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Describe the problem and fix in 2–5 bullets:
app://localhosteven when explicitly listed ingateway.controlUi.allowedOrigins."*"origins.app://localhost) instead of treating them as opaquenullorigins.allowedOrigins: ["app://localhost"]is accepted.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
User-visible / Behavior Changes
gateway.controlUi.allowedOriginsentries likeapp://localhostnow work as expected for Control UI/WebSocket origin checks.Security Impact (required)
No)No)No)No)No)Yes, explain risk + mitigation:Repro + Verification
Environment
gateway.controlUi.allowedOrigins=["app://localhost"]Steps
gateway.controlUi.allowedOrigins=["app://localhost"].Origin: app://localhost.Expected
Actual
origin not allowedbecause parsed origin was normalized tonull.Evidence
Attach at least one:
Human Verification (required)
What you personally verified (not just CI), and how:
app://localhostallowlisting.Compatibility / Migration
Yes)No)No)Failure Recovery (if this breaks)
src/gateway/origin-check.tssrc/gateway/origin-check.test.tsRisks and Mitigations
List only real risks for this PR. Add/remove entries as needed. If none, write
None.