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:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user