Security: Prevent gateway credential exfiltration via URL override (#9179)

* Gateway: require explicit auth for url overrides

* Gateway: scope credential blocking to non-local URLs only

Address review feedback: the previous fix blocked credential fallback for
ALL URL overrides, which was overly strict and could break workflows that
use --url to switch between loopback/tailnet without passing credentials.

Now credential fallback is only blocked for non-local URLs (public IPs,
external hostnames). Local addresses (127.0.0.1, localhost, private IPs
like 192.168.x.x, 10.x.x.x, tailnet 100.x.x.x) still get credential
fallback as before.

This maintains the security fix (preventing credential exfiltration to
attacker-controlled URLs) while preserving backward compatibility for
legitimate local URL overrides.

* Security: require explicit credentials for gateway url overrides (#8113) (thanks @victormier)

* Gateway: reuse explicit auth helper for url overrides (#8113) (thanks @victormier)

* Tests: format gateway chat test (#8113) (thanks @victormier)

* Tests: require explicit auth for gateway url overrides (#8113) (thanks @victormier)

---------

Co-authored-by: Victor Mier <victormier@gmail.com>
This commit is contained in:
Gustavo Madeira Santana
2026-02-04 18:59:44 -05:00
committed by GitHub
parent 96abc1c864
commit a13ff55bd9
12 changed files with 241 additions and 48 deletions

View File

@@ -1,5 +1,6 @@
import { randomUUID } from "node:crypto";
import { loadConfig, resolveGatewayPort } from "../config/config.js";
import { ensureExplicitGatewayAuth, resolveExplicitGatewayAuth } from "../gateway/call.js";
import { GatewayClient } from "../gateway/client.js";
import { GATEWAY_CLIENT_CAPS } from "../gateway/protocol/client-info.js";
import {
@@ -224,33 +225,41 @@ export function resolveGatewayConnection(opts: GatewayConnectionOptions) {
const authToken = config.gateway?.auth?.token;
const localPort = resolveGatewayPort(config);
const urlOverride =
typeof opts.url === "string" && opts.url.trim().length > 0 ? opts.url.trim() : undefined;
const explicitAuth = resolveExplicitGatewayAuth({ token: opts.token, password: opts.password });
ensureExplicitGatewayAuth({
urlOverride,
auth: explicitAuth,
errorHint: "Fix: pass --token or --password when using --url.",
});
const url =
(typeof opts.url === "string" && opts.url.trim().length > 0 ? opts.url.trim() : undefined) ||
urlOverride ||
(typeof remote?.url === "string" && remote.url.trim().length > 0
? remote.url.trim()
: undefined) ||
`ws://127.0.0.1:${localPort}`;
const token =
(typeof opts.token === "string" && opts.token.trim().length > 0
? opts.token.trim()
: undefined) ||
(isRemoteMode
? typeof remote?.token === "string" && remote.token.trim().length > 0
? remote.token.trim()
: undefined
: process.env.OPENCLAW_GATEWAY_TOKEN?.trim() ||
(typeof authToken === "string" && authToken.trim().length > 0
? authToken.trim()
: undefined));
explicitAuth.token ||
(!urlOverride
? isRemoteMode
? typeof remote?.token === "string" && remote.token.trim().length > 0
? remote.token.trim()
: undefined
: process.env.OPENCLAW_GATEWAY_TOKEN?.trim() ||
(typeof authToken === "string" && authToken.trim().length > 0
? authToken.trim()
: undefined)
: undefined);
const password =
(typeof opts.password === "string" && opts.password.trim().length > 0
? opts.password.trim()
: undefined) ||
process.env.OPENCLAW_GATEWAY_PASSWORD?.trim() ||
(typeof remote?.password === "string" && remote.password.trim().length > 0
? remote.password.trim()
explicitAuth.password ||
(!urlOverride
? process.env.OPENCLAW_GATEWAY_PASSWORD?.trim() ||
(typeof remote?.password === "string" && remote.password.trim().length > 0
? remote.password.trim()
: undefined)
: undefined);
return { url, token, password };