Summary
chrome-extension:// origins configured in gateway.controlUi.allowedOrigins are silently rejected because parseOrigin() returns null for them.
Root Cause
In parseOrigin(), the code does:
const url = new URL(trimmed);
return {
origin: url.origin.toLowerCase(),
// ...
};
Node's URL parser returns the string "null" for url.origin when the scheme is chrome-extension://:
new URL("chrome-extension://bfpnaggikhabdgbnhnngdfldkbinncdf").origin
// => "null"
Then earlier in parseOrigin, there's a guard:
if (!trimmed || trimmed === "null") return null;
This doesn't catch it (the raw input isn't "null"), but the returned origin: "null" will never match any entry in the allowlist. The result is a 1008 close with "origin not allowed" even though the exact chrome-extension://... string is in allowedOrigins.
This is particularly confusing because:
- The gateway logs show the correct origin being received
- The config has the correct origin in the allowlist
- There's no indication that the match silently failed due to URL parsing
- Other parts of the codebase already have
chrome-extension:// awareness (e.g., the relay server CORS handling)
Workaround
Patch parseOrigin in the dist files:
sudo sed -i 's|origin: url.origin.toLowerCase()|origin: url.origin === "null" ? trimmed.toLowerCase() : url.origin.toLowerCase()|g' \
/usr/lib/node_modules/openclaw/dist/gateway-cli-CuZs0RlJ.js \
/usr/lib/node_modules/openclaw/dist/gateway-cli-Ol-vpIk7.js
This makes parseOrigin fall back to the raw input string when url.origin is "null", so chrome-extension://... origins match correctly against the allowlist.
Suggested Fix
In parseOrigin(), when url.origin === "null", use the trimmed raw input as the origin instead:
function parseOrigin(originRaw) {
const trimmed = (originRaw ?? "").trim();
if (!trimmed || trimmed === "null") return null;
try {
const url = new URL(trimmed);
const resolved = url.origin === "null" ? trimmed.toLowerCase() : url.origin.toLowerCase();
return {
origin: resolved,
host: url.host.toLowerCase(),
hostname: url.hostname.toLowerCase()
};
} catch {
return null;
}
}
Environment
- OpenClaw v2026.3.13 (npm)
- Node.js v24.14.0
- Chrome extension origin:
chrome-extension://bfpnaggikhabdgbnhnngdfldkbinncdf (OpenClaw Copilot)
- Gateway behind Cloudflare Access tunnel
Summary
chrome-extension://origins configured ingateway.controlUi.allowedOriginsare silently rejected becauseparseOrigin()returnsnullfor them.Root Cause
In
parseOrigin(), the code does:Node's
URLparser returns the string"null"forurl.originwhen the scheme ischrome-extension://:Then earlier in
parseOrigin, there's a guard:This doesn't catch it (the raw input isn't
"null"), but the returnedorigin: "null"will never match any entry in the allowlist. The result is a 1008 close with "origin not allowed" even though the exactchrome-extension://...string is inallowedOrigins.This is particularly confusing because:
chrome-extension://awareness (e.g., the relay server CORS handling)Workaround
Patch
parseOriginin the dist files:sudo sed -i 's|origin: url.origin.toLowerCase()|origin: url.origin === "null" ? trimmed.toLowerCase() : url.origin.toLowerCase()|g' \ /usr/lib/node_modules/openclaw/dist/gateway-cli-CuZs0RlJ.js \ /usr/lib/node_modules/openclaw/dist/gateway-cli-Ol-vpIk7.jsThis makes
parseOriginfall back to the raw input string whenurl.originis"null", sochrome-extension://...origins match correctly against the allowlist.Suggested Fix
In
parseOrigin(), whenurl.origin === "null", use the trimmed raw input as the origin instead:Environment
chrome-extension://bfpnaggikhabdgbnhnngdfldkbinncdf(OpenClaw Copilot)