feat(gateway): deprecate query param hook token auth for security

Query parameter tokens appear in:
- Server access logs
- Browser history
- Referrer headers
- Network monitoring tools

This change adds a deprecation warning when tokens are provided via
query parameter, encouraging migration to header-based authentication
(Authorization: Bearer <token> or X-Clawdbot-Token header).

Changes:
- Modified extractHookToken to return { token, fromQuery } object
- Added deprecation warning in server-http.ts when fromQuery is true
- Updated tests to verify the new return type and fromQuery flag

Fixes #2148

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Yurii Chukhlib 2026-01-26 12:57:51 +01:00 committed by Peter Steinberger
parent e162676e51
commit 94f49f72ec
3 changed files with 27 additions and 9 deletions

View File

@ -47,15 +47,21 @@ describe("gateway hooks helpers", () => {
},
} as unknown as IncomingMessage;
const url = new URL("http://localhost/hooks/wake?token=query");
expect(extractHookToken(req, url)).toBe("top");
const result1 = extractHookToken(req, url);
expect(result1.token).toBe("top");
expect(result1.fromQuery).toBe(false);
const req2 = {
headers: { "x-clawdbot-token": "header" },
} as unknown as IncomingMessage;
expect(extractHookToken(req2, url)).toBe("header");
const result2 = extractHookToken(req2, url);
expect(result2.token).toBe("header");
expect(result2.fromQuery).toBe(false);
const req3 = { headers: {} } as unknown as IncomingMessage;
expect(extractHookToken(req3, url)).toBe("query");
const result3 = extractHookToken(req3, url);
expect(result3.token).toBe("query");
expect(result3.fromQuery).toBe(true);
});
test("normalizeWakePayload trims + validates", () => {

View File

@ -41,21 +41,26 @@ export function resolveHooksConfig(cfg: ClawdbotConfig): HooksConfigResolved | n
};
}
export function extractHookToken(req: IncomingMessage, url: URL): string | undefined {
export type HookTokenResult = {
token: string | undefined;
fromQuery: boolean;
};
export function extractHookToken(req: IncomingMessage, url: URL): HookTokenResult {
const auth =
typeof req.headers.authorization === "string" ? req.headers.authorization.trim() : "";
if (auth.toLowerCase().startsWith("bearer ")) {
const token = auth.slice(7).trim();
if (token) return token;
if (token) return { token, fromQuery: false };
}
const headerToken =
typeof req.headers["x-clawdbot-token"] === "string"
? req.headers["x-clawdbot-token"].trim()
: "";
if (headerToken) return headerToken;
if (headerToken) return { token: headerToken, fromQuery: false };
const queryToken = url.searchParams.get("token");
if (queryToken) return queryToken.trim();
return undefined;
if (queryToken) return { token: queryToken.trim(), fromQuery: true };
return { token: undefined, fromQuery: false };
}
export async function readJsonBody(

View File

@ -76,7 +76,14 @@ export function createHooksRequestHandler(
return false;
}
const token = extractHookToken(req, url);
const { token, fromQuery } = extractHookToken(req, url);
if (fromQuery) {
logHooks.warn(
"Hook token provided via query parameter is deprecated for security reasons. " +
"Tokens in URLs appear in logs, browser history, and referrer headers. " +
"Use Authorization: Bearer <token> or X-Clawdbot-Token header instead.",
);
}
if (!token || token !== hooksConfig.token) {
res.statusCode = 401;
res.setHeader("Content-Type", "text/plain; charset=utf-8");