diff --git a/CHANGELOG.md b/CHANGELOG.md index e7a50c4d50d..16378fd6689 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ Docs: https://docs.openclaw.ai ## Unreleased +### Breaking + +- **BREAKING:** non-loopback Control UI now requires explicit `gateway.controlUi.allowedOrigins` (full origins). Startup fails closed when missing unless `gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true` is set to use Host-header origin fallback mode. + ### Fixes - Security/Config writes: block reserved prototype keys in account-id normalization and route account config resolution through own-key lookups, hardening `/allowlist` and account-scoped config paths against prototype-chain pollution. diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index 5da1c71c09c..8ff41036354 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -2097,6 +2097,8 @@ See [Plugins](/tools/plugin). enabled: true, basePath: "/openclaw", // root: "dist/control-ui", + // allowedOrigins: ["https://control.example.com"], // required for non-loopback Control UI + // dangerouslyAllowHostHeaderOriginFallback: false, // dangerous Host-header origin fallback mode // allowInsecureAuth: false, // dangerouslyDisableDeviceAuth: false, }, @@ -2131,6 +2133,8 @@ See [Plugins](/tools/plugin). - `auth.rateLimit`: optional failed-auth limiter. Applies per client IP and per auth scope (shared-secret and device-token are tracked independently). Blocked attempts return `429` + `Retry-After`. - `auth.rateLimit.exemptLoopback` defaults to `true`; set `false` when you intentionally want localhost traffic rate-limited too (for test setups or strict proxy deployments). - `tailscale.mode`: `serve` (tailnet only, loopback bind) or `funnel` (public, requires auth). +- `controlUi.allowedOrigins`: explicit browser-origin allowlist for Control UI/WebChat WebSocket connects. Required when Control UI is reachable on non-loopback binds. +- `controlUi.dangerouslyAllowHostHeaderOriginFallback`: dangerous mode that enables Host-header origin fallback for deployments that intentionally rely on Host-header origin policy. - `remote.transport`: `ssh` (default) or `direct` (ws/wss). For `direct`, `remote.url` must be `ws://` or `wss://`. - `gateway.remote.token` is for remote CLI calls only; does not enable local gateway auth. - `trustedProxies`: reverse proxy IPs that terminate TLS. Only list proxies you control. diff --git a/docs/gateway/security/index.md b/docs/gateway/security/index.md index 0697ddf25b2..49b985be2a6 100644 --- a/docs/gateway/security/index.md +++ b/docs/gateway/security/index.md @@ -216,6 +216,8 @@ High-signal `checkId` values you will most likely see in real deployments (not e | `gateway.tools_invoke_http.dangerous_allow` | warn/critical | Re-enables dangerous tools over HTTP API | `gateway.tools.allow` | no | | `gateway.nodes.allow_commands_dangerous` | warn/critical | Enables high-impact node commands (camera/screen/contacts/calendar/SMS) | `gateway.nodes.allowCommands` | no | | `gateway.tailscale_funnel` | critical | Public internet exposure | `gateway.tailscale.mode` | no | +| `gateway.control_ui.allowed_origins_required` | critical | Non-loopback Control UI without explicit browser-origin allowlist | `gateway.controlUi.allowedOrigins` | no | +| `gateway.control_ui.host_header_origin_fallback` | warn/critical | Enables Host-header origin fallback (DNS rebinding hardening downgrade) | `gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback` | no | | `gateway.control_ui.insecure_auth` | warn | Insecure-auth compatibility toggle enabled | `gateway.controlUi.allowInsecureAuth` | no | | `gateway.control_ui.device_auth_disabled` | critical | Disables device identity check | `gateway.controlUi.dangerouslyDisableDeviceAuth` | no | | `gateway.real_ip_fallback_enabled` | warn/critical | Trusting `X-Real-IP` fallback can enable source-IP spoofing via proxy misconfig | `gateway.allowRealIpFallback`, `gateway.trustedProxies` | no | @@ -252,6 +254,7 @@ keep it off unless you are actively debugging and can revert quickly. `openclaw security audit` includes `config.insecure_or_dangerous_flags` when any insecure/dangerous debug switches are enabled. This warning aggregates the exact keys so you can review them in one place (for example +`gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true`, `gateway.controlUi.allowInsecureAuth=true`, `gateway.controlUi.dangerouslyDisableDeviceAuth=true`, `hooks.gmail.allowUnsafeExternalContent=true`, or @@ -295,7 +298,8 @@ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - OpenClaw gateway is local/loopback first. If you terminate TLS at a reverse proxy, set HSTS on the proxy-facing HTTPS domain there. - If the gateway itself terminates HTTPS, you can set `gateway.http.securityHeaders.strictTransportSecurity` to emit the HSTS header from OpenClaw responses. - Detailed deployment guidance is in [Trusted Proxy Auth](/gateway/trusted-proxy-auth#tls-termination-and-hsts). -- For non-loopback Control UI deployments, explicitly configure `gateway.controlUi.allowedOrigins` instead of relying on permissive defaults. +- For non-loopback Control UI deployments, `gateway.controlUi.allowedOrigins` is required by default. +- `gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true` enables Host-header origin fallback mode; treat it as a dangerous operator-selected policy. - Treat DNS rebinding and proxy-host header behavior as deployment hardening concerns; keep `trustedProxies` tight and avoid exposing the gateway directly to the public internet. ## Local session logs live on disk diff --git a/docs/web/control-ui.md b/docs/web/control-ui.md index ebaad5aef90..b1ff11c3243 100644 --- a/docs/web/control-ui.md +++ b/docs/web/control-ui.md @@ -233,8 +233,10 @@ Notes: Provide `token` (or `password`) explicitly. Missing explicit credentials is an error. - Use `wss://` when the Gateway is behind TLS (Tailscale Serve, HTTPS proxy, etc.). - `gatewayUrl` is only accepted in a top-level window (not embedded) to prevent clickjacking. -- For cross-origin dev setups (e.g. `pnpm ui:dev` to a remote Gateway), add the UI - origin to `gateway.controlUi.allowedOrigins`. +- Non-loopback Control UI deployments must set `gateway.controlUi.allowedOrigins` + explicitly (full origins). This includes remote dev setups. +- `gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true` enables + Host-header origin fallback mode, but it is a dangerous security mode. Example: diff --git a/docs/web/index.md b/docs/web/index.md index 42baffe8027..3fc48dd993c 100644 --- a/docs/web/index.md +++ b/docs/web/index.md @@ -99,8 +99,10 @@ Open: - Non-loopback binds still **require** a shared token/password (`gateway.auth` or env). - The wizard generates a gateway token by default (even on loopback). - The UI sends `connect.params.auth.token` or `connect.params.auth.password`. -- The Control UI sends anti-clickjacking headers and only accepts same-origin browser - websocket connections unless `gateway.controlUi.allowedOrigins` is set. +- For non-loopback Control UI deployments, set `gateway.controlUi.allowedOrigins` + explicitly (full origins). Without it, gateway startup is refused by default. +- `gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true` enables + Host-header origin fallback mode, but is a dangerous security downgrade. - With Serve, Tailscale identity headers can satisfy Control UI/WebSocket auth when `gateway.auth.allowTailscale` is `true` (no token/password required). HTTP API endpoints still require token/password. Set diff --git a/src/config/schema.help.quality.test.ts b/src/config/schema.help.quality.test.ts index 2980d62eac7..8771090cbff 100644 --- a/src/config/schema.help.quality.test.ts +++ b/src/config/schema.help.quality.test.ts @@ -101,6 +101,7 @@ const TARGET_KEYS = [ "models.providers.*.auth", "models.providers.*.authHeader", "gateway.reload.mode", + "gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback", "gateway.controlUi.allowInsecureAuth", "gateway.controlUi.dangerouslyDisableDeviceAuth", "cron", diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index e5ff4708e09..79a25653380 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -300,7 +300,9 @@ export const FIELD_HELP: Record = { "gateway.controlUi.root": "Optional filesystem root for Control UI assets (defaults to dist/control-ui).", "gateway.controlUi.allowedOrigins": - "Allowed browser origins for Control UI/WebChat websocket connections (full origins only, e.g. https://control.example.com).", + "Allowed browser origins for Control UI/WebChat websocket connections (full origins only, e.g. https://control.example.com). Required for non-loopback Control UI deployments unless dangerous Host-header fallback is explicitly enabled.", + "gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback": + "DANGEROUS toggle that enables Host-header based origin fallback for Control UI/WebChat websocket checks. This mode is supported when your deployment intentionally relies on Host-header origin policy; explicit gateway.controlUi.allowedOrigins remains the recommended hardened default.", "gateway.controlUi.allowInsecureAuth": "Loosens strict browser auth checks for Control UI when you must run a non-standard setup. Keep this off unless you trust your network and proxy path, because impersonation risk is higher.", "gateway.controlUi.dangerouslyDisableDeviceAuth": diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index 5b4130b24eb..986f3c4b3aa 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -238,6 +238,8 @@ export const FIELD_LABELS: Record = { "gateway.controlUi.basePath": "Control UI Base Path", "gateway.controlUi.root": "Control UI Assets Root", "gateway.controlUi.allowedOrigins": "Control UI Allowed Origins", + "gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback": + "Dangerously Allow Host-Header Origin Fallback", "gateway.controlUi.allowInsecureAuth": "Insecure Control UI Auth Toggle", "gateway.controlUi.dangerouslyDisableDeviceAuth": "Dangerously Disable Control UI Device Auth", "gateway.http.endpoints.chatCompletions.enabled": "OpenAI Chat Completions Endpoint", diff --git a/src/config/schema.tags.ts b/src/config/schema.tags.ts index 6e5241b6e9e..82bdc1d87cd 100644 --- a/src/config/schema.tags.ts +++ b/src/config/schema.tags.ts @@ -41,6 +41,12 @@ const TAG_PRIORITY: Record = { const TAG_OVERRIDES: Record = { "gateway.auth.token": ["security", "auth", "access", "network"], "gateway.auth.password": ["security", "auth", "access", "network"], + "gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback": [ + "security", + "access", + "network", + "advanced", + ], "gateway.controlUi.dangerouslyDisableDeviceAuth": ["security", "access", "network", "advanced"], "gateway.controlUi.allowInsecureAuth": ["security", "access", "network", "advanced"], "tools.exec.applyPatch.workspaceOnly": ["tools", "security", "access", "advanced"], diff --git a/src/config/types.gateway.ts b/src/config/types.gateway.ts index edc86b78b0d..5a18da09678 100644 --- a/src/config/types.gateway.ts +++ b/src/config/types.gateway.ts @@ -70,6 +70,11 @@ export type GatewayControlUiConfig = { root?: string; /** Allowed browser origins for Control UI/WebChat websocket connections. */ allowedOrigins?: string[]; + /** + * DANGEROUS: Keep Host-header origin fallback behavior. + * Supported long-term for deployments that intentionally rely on this policy. + */ + dangerouslyAllowHostHeaderOriginFallback?: boolean; /** * Insecure-auth toggle. * Control UI still requires secure context + device identity unless diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts index ea46725ee11..70b528f904c 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -454,6 +454,7 @@ export const OpenClawSchema = z basePath: z.string().optional(), root: z.string().optional(), allowedOrigins: z.array(z.string()).optional(), + dangerouslyAllowHostHeaderOriginFallback: z.boolean().optional(), allowInsecureAuth: z.boolean().optional(), dangerouslyDisableDeviceAuth: z.boolean().optional(), }) diff --git a/src/gateway/origin-check.test.ts b/src/gateway/origin-check.test.ts index 4018903dd08..e267afbf065 100644 --- a/src/gateway/origin-check.test.ts +++ b/src/gateway/origin-check.test.ts @@ -2,14 +2,23 @@ import { describe, expect, it } from "vitest"; import { checkBrowserOrigin } from "./origin-check.js"; describe("checkBrowserOrigin", () => { - it("accepts same-origin host matches", () => { + it("accepts same-origin host matches only with legacy host-header fallback", () => { const result = checkBrowserOrigin({ requestHost: "127.0.0.1:18789", origin: "http://127.0.0.1:18789", + allowHostHeaderOriginFallback: true, }); expect(result.ok).toBe(true); }); + it("rejects same-origin host matches when legacy host-header fallback is disabled", () => { + const result = checkBrowserOrigin({ + requestHost: "gateway.example.com:18789", + origin: "https://gateway.example.com:18789", + }); + expect(result.ok).toBe(false); + }); + it("accepts loopback host mismatches for dev", () => { const result = checkBrowserOrigin({ requestHost: "127.0.0.1:18789", diff --git a/src/gateway/origin-check.ts b/src/gateway/origin-check.ts index 50aea0315ec..7ba20741649 100644 --- a/src/gateway/origin-check.ts +++ b/src/gateway/origin-check.ts @@ -25,6 +25,7 @@ export function checkBrowserOrigin(params: { requestHost?: string; origin?: string; allowedOrigins?: string[]; + allowHostHeaderOriginFallback?: boolean; }): OriginCheckResult { const parsedOrigin = parseOrigin(params.origin); if (!parsedOrigin) { @@ -39,7 +40,11 @@ export function checkBrowserOrigin(params: { } const requestHost = normalizeHostHeader(params.requestHost); - if (requestHost && parsedOrigin.host === requestHost) { + if ( + params.allowHostHeaderOriginFallback === true && + requestHost && + parsedOrigin.host === requestHost + ) { return { ok: true }; } diff --git a/src/gateway/server-runtime-config.test.ts b/src/gateway/server-runtime-config.test.ts index 6d111c40bb1..34cc4632670 100644 --- a/src/gateway/server-runtime-config.test.ts +++ b/src/gateway/server-runtime-config.test.ts @@ -27,6 +27,7 @@ describe("resolveGatewayRuntimeConfig", () => { bind: "lan" as const, auth: TRUSTED_PROXY_AUTH, trustedProxies: ["192.168.1.1"], + controlUi: { allowedOrigins: ["https://control.example.com"] }, }, }, expectedBindHost: "0.0.0.0", @@ -90,7 +91,12 @@ describe("resolveGatewayRuntimeConfig", () => { { name: "lan binding without trusted proxies", cfg: { - gateway: { bind: "lan" as const, auth: TRUSTED_PROXY_AUTH, trustedProxies: [] }, + gateway: { + bind: "lan" as const, + auth: TRUSTED_PROXY_AUTH, + trustedProxies: [], + controlUi: { allowedOrigins: ["https://control.example.com"] }, + }, }, expectedMessage: "gateway auth mode=trusted-proxy requires gateway.trustedProxies to be configured", @@ -121,7 +127,13 @@ describe("resolveGatewayRuntimeConfig", () => { it.each([ { name: "lan binding with token", - cfg: { gateway: { bind: "lan" as const, auth: TOKEN_AUTH } }, + cfg: { + gateway: { + bind: "lan" as const, + auth: TOKEN_AUTH, + controlUi: { allowedOrigins: ["https://control.example.com"] }, + }, + }, expectedAuthMode: "token", expectedBindHost: "0.0.0.0", }, @@ -188,6 +200,36 @@ describe("resolveGatewayRuntimeConfig", () => { expectedMessage, ); }); + + it("rejects non-loopback control UI when allowed origins are missing", async () => { + await expect( + resolveGatewayRuntimeConfig({ + cfg: { + gateway: { + bind: "lan", + auth: TOKEN_AUTH, + }, + }, + port: 18789, + }), + ).rejects.toThrow("non-loopback Control UI requires gateway.controlUi.allowedOrigins"); + }); + + it("allows non-loopback control UI without allowed origins when dangerous fallback is enabled", async () => { + const result = await resolveGatewayRuntimeConfig({ + cfg: { + gateway: { + bind: "lan", + auth: TOKEN_AUTH, + controlUi: { + dangerouslyAllowHostHeaderOriginFallback: true, + }, + }, + }, + port: 18789, + }); + expect(result.bindHost).toBe("0.0.0.0"); + }); }); describe("HTTP security headers", () => { diff --git a/src/gateway/server-runtime-config.ts b/src/gateway/server-runtime-config.ts index 5ddd9b789a5..d6352edf6a3 100644 --- a/src/gateway/server-runtime-config.ts +++ b/src/gateway/server-runtime-config.ts @@ -115,6 +115,11 @@ export async function resolveGatewayRuntimeConfig(params: { process.env.OPENCLAW_SKIP_CANVAS_HOST !== "1" && params.cfg.canvasHost?.enabled !== false; const trustedProxies = params.cfg.gateway?.trustedProxies ?? []; + const controlUiAllowedOrigins = (params.cfg.gateway?.controlUi?.allowedOrigins ?? []) + .map((value) => value.trim()) + .filter(Boolean); + const dangerouslyAllowHostHeaderOriginFallback = + params.cfg.gateway?.controlUi?.dangerouslyAllowHostHeaderOriginFallback === true; assertGatewayAuthConfigured(resolvedAuth); if (tailscaleMode === "funnel" && authMode !== "password") { @@ -130,6 +135,16 @@ export async function resolveGatewayRuntimeConfig(params: { `refusing to bind gateway to ${bindHost}:${params.port} without auth (set gateway.auth.token/password, or set OPENCLAW_GATEWAY_TOKEN/OPENCLAW_GATEWAY_PASSWORD)`, ); } + if ( + controlUiEnabled && + !isLoopbackHost(bindHost) && + controlUiAllowedOrigins.length === 0 && + !dangerouslyAllowHostHeaderOriginFallback + ) { + throw new Error( + "non-loopback Control UI requires gateway.controlUi.allowedOrigins (set explicit origins), or set gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true to use Host-header origin fallback mode", + ); + } if (authMode === "trusted-proxy") { if (trustedProxies.length === 0) { diff --git a/src/gateway/server/ws-connection/message-handler.ts b/src/gateway/server/ws-connection/message-handler.ts index eb62269c85e..1798b71afb4 100644 --- a/src/gateway/server/ws-connection/message-handler.ts +++ b/src/gateway/server/ws-connection/message-handler.ts @@ -334,6 +334,8 @@ export function attachGatewayWsMessageHandler(params: { requestHost, origin: requestOrigin, allowedOrigins: configSnapshot.gateway?.controlUi?.allowedOrigins, + allowHostHeaderOriginFallback: + configSnapshot.gateway?.controlUi?.dangerouslyAllowHostHeaderOriginFallback === true, }); if (!originCheck.ok) { const errorMessage = diff --git a/src/security/audit.test.ts b/src/security/audit.test.ts index 6915855b1e8..2b4fbebe033 100644 --- a/src/security/audit.test.ts +++ b/src/security/audit.test.ts @@ -1136,6 +1136,38 @@ describe("security audit", () => { expect(finding?.detail).toContain("tools.exec.applyPatch.workspaceOnly=false"); }); + it("flags non-loopback Control UI without allowed origins", async () => { + const cfg: OpenClawConfig = { + gateway: { + bind: "lan", + auth: { mode: "token", token: "very-long-browser-token-0123456789" }, + }, + }; + + const res = await audit(cfg); + expectFinding(res, "gateway.control_ui.allowed_origins_required", "critical"); + }); + + it("flags dangerous host-header origin fallback and suppresses missing allowed-origins finding", async () => { + const cfg: OpenClawConfig = { + gateway: { + bind: "lan", + auth: { mode: "token", token: "very-long-browser-token-0123456789" }, + controlUi: { + dangerouslyAllowHostHeaderOriginFallback: true, + }, + }, + }; + + const res = await audit(cfg); + expectFinding(res, "gateway.control_ui.host_header_origin_fallback", "critical"); + expectNoFinding(res, "gateway.control_ui.allowed_origins_required"); + const flags = res.findings.find((f) => f.checkId === "config.insecure_or_dangerous_flags"); + expect(flags?.detail ?? "").toContain( + "gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true", + ); + }); + it("scores X-Real-IP fallback risk by gateway exposure", async () => { const trustedProxyCfg = (trustedProxies: string[]): OpenClawConfig => ({ gateway: { diff --git a/src/security/audit.ts b/src/security/audit.ts index e07fff18982..6d4aa90d380 100644 --- a/src/security/audit.ts +++ b/src/security/audit.ts @@ -266,6 +266,11 @@ function collectGatewayConfigFindings( const tailscaleMode = cfg.gateway?.tailscale?.mode ?? "off"; const auth = resolveGatewayAuth({ authConfig: cfg.gateway?.auth, tailscaleMode, env }); const controlUiEnabled = cfg.gateway?.controlUi?.enabled !== false; + const controlUiAllowedOrigins = (cfg.gateway?.controlUi?.allowedOrigins ?? []) + .map((value) => value.trim()) + .filter(Boolean); + const dangerouslyAllowHostHeaderOriginFallback = + cfg.gateway?.controlUi?.dangerouslyAllowHostHeaderOriginFallback === true; const trustedProxies = Array.isArray(cfg.gateway?.trustedProxies) ? cfg.gateway.trustedProxies : []; @@ -340,6 +345,37 @@ function collectGatewayConfigFindings( remediation: "Set gateway.auth (token recommended) or keep the Control UI local-only.", }); } + if ( + bind !== "loopback" && + controlUiEnabled && + controlUiAllowedOrigins.length === 0 && + !dangerouslyAllowHostHeaderOriginFallback + ) { + findings.push({ + checkId: "gateway.control_ui.allowed_origins_required", + severity: "critical", + title: "Non-loopback Control UI missing explicit allowed origins", + detail: + "Control UI is enabled on a non-loopback bind but gateway.controlUi.allowedOrigins is empty. " + + "Strict origin policy requires explicit allowed origins for non-loopback deployments.", + remediation: + "Set gateway.controlUi.allowedOrigins to full trusted origins (for example https://control.example.com). " + + "If your deployment intentionally relies on Host-header origin fallback, set gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true.", + }); + } + if (dangerouslyAllowHostHeaderOriginFallback) { + const exposed = bind !== "loopback"; + findings.push({ + checkId: "gateway.control_ui.host_header_origin_fallback", + severity: exposed ? "critical" : "warn", + title: "DANGEROUS: Host-header origin fallback enabled", + detail: + "gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true enables Host-header origin fallback " + + "for Control UI/WebChat websocket checks and weakens DNS rebinding protections.", + remediation: + "Disable gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback and configure explicit gateway.controlUi.allowedOrigins.", + }); + } if (allowRealIpFallback) { const hasNonLoopbackTrustedProxy = trustedProxies.some( diff --git a/src/security/dangerous-config-flags.ts b/src/security/dangerous-config-flags.ts index a272d5a069a..1dbbd39cc31 100644 --- a/src/security/dangerous-config-flags.ts +++ b/src/security/dangerous-config-flags.ts @@ -5,6 +5,9 @@ export function collectEnabledInsecureOrDangerousFlags(cfg: OpenClawConfig): str if (cfg.gateway?.controlUi?.allowInsecureAuth === true) { enabledFlags.push("gateway.controlUi.allowInsecureAuth=true"); } + if (cfg.gateway?.controlUi?.dangerouslyAllowHostHeaderOriginFallback === true) { + enabledFlags.push("gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true"); + } if (cfg.gateway?.controlUi?.dangerouslyDisableDeviceAuth === true) { enabledFlags.push("gateway.controlUi.dangerouslyDisableDeviceAuth=true"); }