mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-05 22:32:12 +00:00
fix(mattermost): harden slash command token validation
This commit is contained in:
@@ -180,6 +180,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Providers/OpenRouter: gate Anthropic prompt-cache `cache_control` markers to native/default OpenRouter routes and preserve them for native OpenRouter hosts behind custom provider ids. Thanks @vincentkoc.
|
||||
- Browser/CDP: validate both initial and discovered CDP websocket endpoints before connect so strict SSRF policy blocks cross-host pivots and direct websocket targets. (#60469) Thanks @eleqtrizit.
|
||||
- Browser/profiles: reject remote browser profile `cdpUrl` values that violate strict SSRF policy before saving config, with clearer validation errors for blocked endpoints. (#60477) Thanks @eleqtrizit.
|
||||
- Mattermost/slash commands: harden native slash-command callback token validation to use constant-time secret comparison, matching the existing interaction-token path.
|
||||
|
||||
## 2026.4.1
|
||||
|
||||
|
||||
@@ -236,4 +236,29 @@ describe("slash-http cfg threading", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not rely on Set.has for command token validation", async () => {
|
||||
const commandTokens = new Set(["valid-token"]);
|
||||
const hasSpy = vi.fn(() => {
|
||||
throw new Error("Set.has should not be used for slash token validation");
|
||||
});
|
||||
Object.defineProperty(commandTokens, "has", {
|
||||
value: hasSpy,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const handler = createSlashCommandHttpHandler({
|
||||
account: accountFixture,
|
||||
cfg: {} as OpenClawConfig,
|
||||
runtime: {} as RuntimeEnv,
|
||||
commandTokens,
|
||||
});
|
||||
const response = createResponse();
|
||||
|
||||
await handler(createRequest(), response.res);
|
||||
|
||||
expect(response.res.statusCode).toBe(200);
|
||||
expect(response.getBody()).toContain("Processing");
|
||||
expect(hasSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { safeEqualSecret } from "openclaw/plugin-sdk/browser-support";
|
||||
import type { ResolvedMattermostAccount } from "../mattermost/accounts.js";
|
||||
import { getMattermostRuntime } from "../runtime.js";
|
||||
import {
|
||||
@@ -78,6 +79,18 @@ function sendJsonResponse(
|
||||
res.end(JSON.stringify(body));
|
||||
}
|
||||
|
||||
function matchesRegisteredCommandToken(
|
||||
commandTokens: ReadonlySet<string>,
|
||||
candidate: string,
|
||||
): boolean {
|
||||
for (const token of commandTokens) {
|
||||
if (safeEqualSecret(candidate, token)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
type SlashInvocationAuth = {
|
||||
ok: boolean;
|
||||
denyResponse?: MattermostSlashCommandResponse;
|
||||
@@ -242,7 +255,7 @@ export function createSlashCommandHttpHandler(params: SlashHttpHandlerParams) {
|
||||
|
||||
// Validate token — fail closed: reject when no tokens are registered
|
||||
// (e.g. registration failed or startup was partial)
|
||||
if (commandTokens.size === 0 || !commandTokens.has(payload.token)) {
|
||||
if (commandTokens.size === 0 || !matchesRegisteredCommandToken(commandTokens, payload.token)) {
|
||||
sendJsonResponse(res, 401, {
|
||||
response_type: "ephemeral",
|
||||
text: "Unauthorized: invalid command token.",
|
||||
|
||||
Reference in New Issue
Block a user