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

* 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>

* fix: deprecate hook query token auth (#2200) (thanks @YuriNachos)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Yuri Chukhlib
2026-01-26 15:51:25 +01:00
committed by GitHub
parent f3e3c4573b
commit 961b4adc1c
5 changed files with 32 additions and 13 deletions

View File

@@ -6,6 +6,7 @@ Docs: https://docs.clawd.bot
Status: unreleased.
### Changes
- Gateway: warn on hook tokens via query params; document header auth preference. (#2200) Thanks @YuriNachos.
- Doctor: warn on gateway exposure without auth. (#2016) Thanks @Alex-Alaniz.
- Docs: add Vercel AI Gateway to providers sidebar. (#1901) Thanks @jerilynzheng.
- Agents: expand cron tool description with full schema docs. (#1988) Thanks @tomascupr.

View File

@@ -27,10 +27,10 @@ Notes:
## Auth
Every request must include the hook token:
- `Authorization: Bearer <token>`
- or `x-clawdbot-token: <token>`
- or `?token=<token>`
Every request must include the hook token. Prefer headers:
- `Authorization: Bearer <token>` (recommended)
- `x-clawdbot-token: <token>`
- `?token=<token>` (deprecated; logs a warning and will be removed in a future major release)
## Endpoints

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,13 +76,20 @@ export function createHooksRequestHandler(
return false;
}
const token = extractHookToken(req, url);
const { token, fromQuery } = extractHookToken(req, url);
if (!token || token !== hooksConfig.token) {
res.statusCode = 401;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("Unauthorized");
return true;
}
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 (req.method !== "POST") {
res.statusCode = 405;