Skip to content

[Bug]: Mattermost slash commands default to cleartext callback URLs that expose reusable command tokens #65624

@coygeek

Description

@coygeek

Severity Assessment

CVSS Assessment

Metric v3.1 v4.0
Score 7.6 / 10.0 8.6 / 10.0
Severity High High
Vector CVSS:3.1/AV:A/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:L CVSS:4.0/AV:A/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:L/SC:N/SI:N/SA:N
Calculator CVSS v3.1 Calculator CVSS v4.0 Calculator

Threat Model Alignment

Classification: security-specific

OpenClaw’s trust model treats Mattermost slash callbacks as a protected webhook surface: the docs require an explicit HTTPS callbackUrl, state that non-HTTPS derived callbacks must fail closed, and authenticate callback requests with Mattermost-issued command tokens on a plugin route. A cleartext fallback callback therefore is not an intended public capability or harmless operator convenience; it weakens the transport and token-auth boundary that guards command execution. That makes the finding security-specific rather than a generic hardening request.

Impact

When Mattermost native slash commands are enabled without an explicit HTTPS callback URL, OpenClaw auto-registers a plain-HTTP callback and then treats any captured slash-command token as valid for every registered command. An on-path attacker can replay that token to inject arbitrary /oc_* or native skill commands as a spoofed Mattermost sender.

Affected Component

Files: extensions/mattermost/src/mattermost/monitor-slash.ts:151-180, extensions/mattermost/src/mattermost/slash-commands.ts:540-572, extensions/mattermost/src/mattermost/slash-state.ts:94-117, extensions/mattermost/src/mattermost/slash-http.ts:243-255

// slash-commands.ts
export function resolveCallbackUrl(params: {...}): string {
  if (params.config.callbackUrl) {
    return params.config.callbackUrl;
  }
  ...
  return `http://${host}:${params.gatewayPort}${path}`;
}

// monitor-slash.ts
const slashCallbackUrl = resolveCallbackUrl({
  config: slashConfig,
  gatewayPort: slashGatewayPort,
  gatewayHost: params.cfg.gateway?.customBindHost ?? undefined,
});

// slash-state.ts
const tokenSet = new Set(commandTokens);
const handler = createSlashCommandHttpHandler({ ..., commandTokens: tokenSet, ... });

// slash-http.ts
if (commandTokens.size === 0 || !commandTokens.has(payload.token)) {
  sendJsonResponse(res, 401, { text: "Unauthorized: invalid command token." });
  return;
}
const trigger = payload.command.replace(/^\//, "").trim();
const commandText = resolveCommandText(trigger, payload.text, triggerMap);

Technical Reproduction

  1. Configure Mattermost native slash commands, set gateway.customBindHost to a host that the Mattermost server can reach, and omit channels.mattermost.commands.callbackUrl.
  2. Start OpenClaw. registerMattermostMonitorSlashCommands() derives and registers a callback like http://gateway.example.com:18789/api/channels/mattermost/command.
  3. Invoke any legitimate slash command once from Mattermost while observing the callback traffic on the network path between Mattermost and OpenClaw.
  4. Capture the POST body token, then replay a forged request directly to /api/channels/mattermost/command, for example:
    token=<captured-token>&team_id=T1&channel_id=C1&user_id=<allowlisted-user>&user_name=owner&command=/oc_model&text=gpt-5.4
    
  5. Observe that createSlashCommandHttpHandler() accepts the request because it only checks whether payload.token is in the per-account token set, then executes the attacker-controlled command, text, channel_id, and user_id values.

Demonstrated Impact

resolveCallbackUrl() emits http://... whenever operators rely on the default derived callback, so the reusable Mattermost command token is transported in cleartext. activateSlashCommands() then discards the trigger-to-token mapping and keeps only a Set<string> of valid tokens, and createSlashCommandHttpHandler() authorizes any callback whose token is present in that set. Because the callback route is intentionally registered as auth: "plugin", the attacker does not need gateway bearer auth once a single token is observed. A passive or active network attacker can therefore capture one slash-command callback, then forge arbitrary /oc_* or native skill command payloads under any sender/channel values that satisfy Mattermost channel policy.

Environment

Tested against 87eeab703465eb42481946b130b9122c3e0fb587 on current main, with the bundled Mattermost plugin, channels.mattermost.commands.native=true, a reachable non-loopback gateway.customBindHost, and no explicit channels.mattermost.commands.callbackUrl override.

Remediation Advice

Fail closed when the derived Mattermost slash-command callback is not HTTPS, and preserve per-command token bindings so each callback token only authorizes the trigger that Mattermost issued it for.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions