Skip to content

fix: harden Mattermost slash callback auth#65655

Open
coygeek wants to merge 1 commit intoopenclaw:mainfrom
coygeek:fix/65624-mattermost-slash-callback-auth
Open

fix: harden Mattermost slash callback auth#65655
coygeek wants to merge 1 commit intoopenclaw:mainfrom
coygeek:fix/65624-mattermost-slash-callback-auth

Conversation

@coygeek
Copy link
Copy Markdown
Contributor

@coygeek coygeek commented Apr 13, 2026

Fix Summary

Mattermost native slash commands were insecure in two ways: when commands.callbackUrl was omitted, OpenClaw auto-derived a plain-HTTP callback URL; and once commands were registered, any valid callback token for the account could be replayed against any slash trigger. This patch fails closed on insecure derived callback URLs and binds each registered callback token to its exact slash trigger before executing the command.

Issue Linkage

Fixes #65624

Security Snapshot

  • CVSS v3.1: 7.6 (High)
  • CVSS v4.0: 8.6 (High)

Implementation Details

Files Changed

  • docs/channels/mattermost.md (+4/-1)
  • docs/gateway/configuration-reference.md (+2/-0)
  • extensions/mattermost/src/mattermost/monitor-slash.test.ts (+25/-11)
  • extensions/mattermost/src/mattermost/monitor-slash.ts (+13/-0)
  • extensions/mattermost/src/mattermost/slash-http.send-config.test.ts (+2/-0)
  • extensions/mattermost/src/mattermost/slash-http.test.ts (+17/-2)
  • extensions/mattermost/src/mattermost/slash-http.ts (+24/-1)
  • extensions/mattermost/src/mattermost/slash-state.ts (+12/-0)

Technical Analysis

The slash registration path previously accepted a derived http://... callback when operators omitted commands.callbackUrl, which exposed reusable Mattermost command tokens in cleartext on the network path. Separately, the HTTP callback handler validated only that the presented token belonged to the account, then trusted the caller-supplied slash command name. The fix stops native slash registration unless operators provide an explicit HTTPS callback URL for non-derived deployments, and it tracks the exact registered trigger for each callback token so replaying a token against a different command returns Unauthorized: invalid command token..

Validation Evidence

  • Command: pnpm test extensions/mattermost/src/mattermost/slash-http.test.ts
  • Status: passed
  • Command: pnpm test extensions/mattermost/src/mattermost/slash-http.send-config.test.ts
  • Status: passed
  • Command: pnpm test extensions/mattermost/src/mattermost/monitor-slash.test.ts
  • Status: passed

Risk and Compatibility

  • behavior change for insecure setups: native slash command auto-registration now stops until operators configure an explicit reachable HTTPS commands.callbackUrl; valid callbacks keep working once configured

AI-Assisted Disclosure

  • AI-assisted: yes
  • Model: github-copilot/gpt-5.4

@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation channel: mattermost Channel integration: mattermost gateway Gateway runtime size: S labels Apr 13, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 13, 2026

Greptile Summary

This PR hardens Mattermost native slash command auth in two ways: it fails closed on registration when callbackUrl is omitted and the derived fallback is non-HTTPS, and it binds each registered callback token to its exact slash trigger so a captured token cannot be replayed against a different command. Both fixes are targeted, tests pass, and constant-time comparison (safeEqualSecret) is used correctly in the trigger-resolver loop. Two minor P2 gaps remain: the commandTokenTriggers parameter is typed as optional so the cross-trigger protection silently no-ops if a handler is ever constructed without it, and the HTTPS guard only fires for derived URLs — operators who explicitly configure an http:// callback bypass it with no warning.

Confidence Score: 5/5

Safe to merge; both security fixes are correct and well-tested, remaining findings are P2 style concerns only.

All active findings are P2 (silent optional bypass is a latent risk not a current defect; explicit-HTTP gap is a hardening suggestion). Production code always provides commandTokenTriggers. No P0/P1 issues identified.

No files require special attention beyond the two P2 comments on slash-http.ts and monitor-slash.ts.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/mattermost/src/mattermost/slash-http.ts
Line: 55

Comment:
**Silent bypass when `commandTokenTriggers` is absent**

`commandTokenTriggers` is typed as optional, so when it's `undefined`, `resolveRegisteredCommandTrigger` returns `undefined` and the `if (expectedTrigger && expectedTrigger !== trigger)` guard is never entered — cross-trigger replay protection is silently dropped. Production code via `activateSlashCommands` always provides the map, but a handler constructed without it (e.g., in a future code path) would revert to token-only validation without any warning or compile-time signal. Making the field required, or at minimum adding a fallthrough `runtime.error` when the map is absent, would prevent silent regressions.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/mattermost/src/mattermost/monitor-slash.ts
Line: 171-176

Comment:
**No warning for explicitly-configured non-HTTPS callback URLs**

The new guard fires only when `callbackUrl` is *omitted* (`!slashConfig.callbackUrl`). If an operator explicitly sets `callbackUrl: "http://public-server.example.com/slash"`, both conditions are false, the block is skipped, and registration proceeds silently over HTTP. `warnOnSuspiciousCallbackUrl` won't catch this either — it only warns about loopback hosts. Adding a warning (or a fail-closed error matching the derived-URL path) when an explicit `callbackUrl` resolves to non-HTTPS would close this gap and keep the behaviour consistent with the documented intent.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix: harden Mattermost slash callback au..." | Re-trigger Greptile

/** Expected token from registered commands (for validation). */
commandTokens: Set<string>;
/** Exact trigger expected for each registered token. */
commandTokenTriggers?: ReadonlyMap<string, string>;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Silent bypass when commandTokenTriggers is absent

commandTokenTriggers is typed as optional, so when it's undefined, resolveRegisteredCommandTrigger returns undefined and the if (expectedTrigger && expectedTrigger !== trigger) guard is never entered — cross-trigger replay protection is silently dropped. Production code via activateSlashCommands always provides the map, but a handler constructed without it (e.g., in a future code path) would revert to token-only validation without any warning or compile-time signal. Making the field required, or at minimum adding a fallthrough runtime.error when the map is absent, would prevent silent regressions.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/mattermost/src/mattermost/slash-http.ts
Line: 55

Comment:
**Silent bypass when `commandTokenTriggers` is absent**

`commandTokenTriggers` is typed as optional, so when it's `undefined`, `resolveRegisteredCommandTrigger` returns `undefined` and the `if (expectedTrigger && expectedTrigger !== trigger)` guard is never entered — cross-trigger replay protection is silently dropped. Production code via `activateSlashCommands` always provides the map, but a handler constructed without it (e.g., in a future code path) would revert to token-only validation without any warning or compile-time signal. Making the field required, or at minimum adding a fallthrough `runtime.error` when the map is absent, would prevent silent regressions.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +171 to +176
if (!slashConfig.callbackUrl && !isHttpsUrl(slashCallbackUrl)) {
params.runtime.error?.(
`mattermost: native slash commands require an explicit HTTPS channels.mattermost.commands.callbackUrl; refusing derived callback ${slashCallbackUrl}`,
);
return;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No warning for explicitly-configured non-HTTPS callback URLs

The new guard fires only when callbackUrl is omitted (!slashConfig.callbackUrl). If an operator explicitly sets callbackUrl: "http://public-server.example.com/slash", both conditions are false, the block is skipped, and registration proceeds silently over HTTP. warnOnSuspiciousCallbackUrl won't catch this either — it only warns about loopback hosts. Adding a warning (or a fail-closed error matching the derived-URL path) when an explicit callbackUrl resolves to non-HTTPS would close this gap and keep the behaviour consistent with the documented intent.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/mattermost/src/mattermost/monitor-slash.ts
Line: 171-176

Comment:
**No warning for explicitly-configured non-HTTPS callback URLs**

The new guard fires only when `callbackUrl` is *omitted* (`!slashConfig.callbackUrl`). If an operator explicitly sets `callbackUrl: "http://public-server.example.com/slash"`, both conditions are false, the block is skipped, and registration proceeds silently over HTTP. `warnOnSuspiciousCallbackUrl` won't catch this either — it only warns about loopback hosts. Adding a warning (or a fail-closed error matching the derived-URL path) when an explicit `callbackUrl` resolves to non-HTTPS would close this gap and keep the behaviour consistent with the documented intent.

How can I resolve this? If you propose a fix, please make it concise.

@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 27, 2026

Codex review: found issues before merge.

What this changes:

This PR updates Mattermost slash command docs, registration, HTTP handler, state, and tests to fail closed for non-HTTPS derived callbacks and bind registered command tokens to slash triggers.

Maintainer follow-up before merge:

This is security-sensitive Mattermost auth work, paired with open bug #65624 and stale against merged #72923, so maintainers should choose whether to rebase this branch, close it in favor of a narrow follow-up, or make a policy decision for non-HTTPS callback URLs.

Security review:

Security review needs attention: The diff improves Mattermost slash callback auth, but leaves concrete cleartext and silent-bypass gaps in the proposed hardening.

Review findings:

  • [P2] Reject explicit non-HTTPS callback URLs — extensions/mattermost/src/mattermost/monitor-slash.ts:168-174
  • [P2] Make trigger binding fail closed — extensions/mattermost/src/mattermost/slash-http.ts:55
Review details

Best possible solution:

Keep the current #72923 command-validation architecture and add a narrow follow-up on top of main that enforces the chosen Mattermost callback URL transport policy, with docs and focused tests for omitted callbackUrl, explicit http:// callbackUrl, and any allowed local/private exceptions.

Do we have a high-confidence way to reproduce the issue?

Yes. A source-level reproduction is high confidence: enable Mattermost native slash commands, omit channels.mattermost.commands.callbackUrl, set a reachable gateway host, and current main derives an http://... callback and proceeds to registration.

Is this the best way to solve the issue?

No, not as submitted. The PR addresses the right security surface, but current main has a stronger replacement for token replay validation, while the PR's callback URL guard is incomplete and should be rebased or replaced with a narrow policy fix on current main.

Full review comments:

  • [P2] Reject explicit non-HTTPS callback URLs — extensions/mattermost/src/mattermost/monitor-slash.ts:168-174
    The new guard only runs when callbackUrl is omitted. If an operator explicitly configures an http:// callback, registration still proceeds and sends Mattermost command tokens over cleartext, despite the new docs and PR rationale requiring HTTPS. Apply the same fail-closed or documented exception policy to the resolved URL.
    Confidence: 0.9
  • [P2] Make trigger binding fail closed — extensions/mattermost/src/mattermost/slash-http.ts:55
    commandTokenTriggers is optional, so a handler constructed without it silently falls back to token-only validation and accepts a valid token for any trigger. Since this patch relies on token-to-trigger binding for the security fix, make the binding required or reject callbacks when the map is missing.
    Confidence: 0.84

Overall correctness: patch is incorrect
Overall confidence: 0.88

Security concerns:

  • [medium] Explicit HTTP callback URLs still register — extensions/mattermost/src/mattermost/monitor-slash.ts:168
    The fail-closed check is limited to derived URLs, so explicitly configured http:// callbacks continue to expose reusable Mattermost command tokens over cleartext transport.
    Confidence: 0.9
  • [medium] Trigger binding can silently no-op — extensions/mattermost/src/mattermost/slash-http.ts:55
    The proposed commandTokenTriggers parameter is optional; when absent, the new cross-trigger replay protection is disabled without an error.
    Confidence: 0.84

Acceptance criteria:

  • pnpm test extensions/mattermost/src/mattermost/slash-http.test.ts extensions/mattermost/src/mattermost/slash-http.send-config.test.ts extensions/mattermost/src/mattermost/slash-state.test.ts extensions/mattermost/src/mattermost/monitor-slash.test.ts extensions/mattermost/src/mattermost/slash-commands.test.ts
  • pnpm check:changed

What I checked:

  • Current main still derives an HTTP callback fallback: resolveCallbackUrl returns the configured callbackUrl when present, otherwise builds http://${host}:${gatewayPort}${path}. (extensions/mattermost/src/mattermost/slash-commands.ts:570, 4babd925c40d)
  • Current main still proceeds to registration with the resolved callback URL: registerMattermostMonitorSlashCommands resolves the callback URL, calls only warnOnSuspiciousCallbackUrl, then passes the URL into registerSlashCommandsAcrossTeams; there is no HTTPS fail-closed guard. (extensions/mattermost/src/mattermost/monitor-slash.ts:157, 4babd925c40d)
  • Current main now has stronger per-command callback validation: The handler maps payloads to a registered (team, trigger), rejects tokens that do not match that command, then validates the current Mattermost command id, team, trigger, method, URL, and token before processing. (extensions/mattermost/src/mattermost/slash-http.ts:582, 4babd925c40d)
  • Current tests cover token replay and current-command drift: Regression tests reject a token valid for one command when used against another, reject rotated current command tokens, and reject current commands with mismatched method or callback URL. (extensions/mattermost/src/mattermost/slash-http.test.ts:202, 4babd925c40d)
  • Current docs still describe deriving a callback URL when omitted: The Mattermost docs say OpenClaw derives a callback from gateway host/port plus callbackPath when callbackUrl is omitted, and troubleshooting still treats the HTTP loopback fallback as a reachability warning rather than a fail-closed condition. Public docs: docs/channels/mattermost.md. (docs/channels/mattermost.md:93, 4babd925c40d)
  • Related merged PR only covers the callback validation half: Provided GitHub context shows Mattermost: refresh slash callback command validation #72923 merged on 2026-05-01 with current command id/team/trigger/method/URL/token validation and regression coverage; current main matches that architecture, but it does not include this PR's derived HTTPS fail-closed guard. (250b3e778d02)

Likely related people:

  • Peter Steinberger: Blame and file history point to recent Mattermost slash-command validation and docs maintenance in the current snapshot, and the path shortlog shows the largest recent share of touches across the central Mattermost slash files. (role: recent maintainer; confidence: high; commits: 0640db72b059, 66385670e43f, 2a65bfee966e; files: extensions/mattermost/src/mattermost/slash-http.ts, extensions/mattermost/src/mattermost/monitor-slash.ts, extensions/mattermost/src/mattermost/slash-commands.ts)
  • Muhammed Mukhthar CM: The native Mattermost slash command feature appears to date to feat(mattermost): add native slash command support (refresh), which introduced the relevant slash command registration surface. (role: introduced behavior; confidence: medium; commits: b1b41eb44323; files: extensions/mattermost/src/mattermost/slash-http.ts, extensions/mattermost/src/mattermost/slash-state.ts, extensions/mattermost/src/mattermost/slash-commands.ts)
  • eleqtrizit: Provided PR context shows this contributor authored the merged Mattermost: refresh slash callback command validation #72923 refresh that current main uses for Mattermost slash callback command validation, which directly overlaps the token replay portion of this PR. (role: adjacent owner; confidence: medium; commits: 250b3e778d02; files: extensions/mattermost/src/mattermost/slash-http.ts, extensions/mattermost/src/mattermost/slash-state.ts, extensions/mattermost/src/mattermost/slash-commands.ts)
  • Vincent Koc: History shows adjacent webhook and Mattermost private-network/security work in nearby transport and auth-sensitive paths, making this a plausible secondary route for review. (role: adjacent maintainer; confidence: low; commits: 5e78c8bc95d8, c863ee1b86c1, d3fc6c0cc79f; files: extensions/mattermost/src/mattermost/slash-http.ts, extensions/mattermost/src/mattermost/monitor-slash.ts, extensions/mattermost/src/mattermost/slash-commands.ts)

Remaining risk / open question:

  • The remaining callback transport policy is security-sensitive: maintainers should decide whether to reject all non-HTTPS callback URLs, only reject derived non-HTTPS URLs, or document an explicit operator escape hatch.
  • The PR is stale against the Mattermost: refresh slash callback command validation #72923 current-command validation architecture, so applying it as-is would be a poor path even though the derived-callback guard is still relevant.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 4babd925c40d.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: mattermost Channel integration: mattermost docs Improvements or additions to documentation gateway Gateway runtime size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant