Mattermost CORE /api/v4 differential IDOR/BAC: WALK CLEAN (fallback after GitLab)
Date: 2026-06-23
Target: Mattermost (mattermost-preview, bundled DB), self-hosted docker (mm-fuzz, :8065), disposable
Tool: fuzzer-arm v0.3 (new adapter targets/mattermost.py + run_mattermost.py)
Cash-gate: Mattermost is on a PAYING Bugcrowd program (already cash-confirmed - we filed the
Jira-integration BAC there). We had ONLY ever fuzzed Mattermost PLUGINS; the CORE /api/v4 was
never differential-fuzzed by us -> net-new surface. Self-hosted = in-ROE.
Why this run (directive fallback)
GitLab CE full surface (1179 endpoints) came back genuinely clean -> rotate to Mattermost CORE.
Setup (banked recipe - Mattermost is now a supported fuzzer-arm target)
- Auth: session-token, NOT PAT. POST /api/v4/users/login {login_id,password} -> token in the
Token
response HEADER (not body) -> use Authorization: Bearer <token>. (core.py scheme!='gitea'/'gitlab'
already yields Bearer, so SCHEME='mattermost' just works.)
- First user created on a fresh install becomes system admin (POST /api/v4/users, no auth) -> then
create alice/bob/carol with the admin token.
- Tenancy: teams (type 'I' invite-only) contain channels; private channels type 'P' = members-only.
A=alice owns alice-team + private channel + marked post + uploaded file. B=bob is in a SEPARATE team,
NOT a member of alice's team/channel. Differential replays alice's reads/writes as bob w/ alice's ids.
- File upload returns ids under
file_infos[0].id (multipart files= + data={channel_id}).
Result: CLEAN
- 39 curated CORE endpoints tested: scoped-ok=31, inconclusive=7, IDOR-weak=1. 0 confirmed IDOR, 0 BAC.
- EVERY core private pivot: A=200 (alice), B=403 (bob blocked) -> channel, channel/posts, members,
stats, pinned, timezones, team, team/channels, team/members, post, thread, reactions, file, file/info.
- WRITE-BAC (path-only DELETEs): bob delete alice's post / channel / team / kick-alice -> all B=403.
- Mattermost core authz is solid: a non-member of the team/private-channel gets a clean 403 on every
one of alice's private resources. The cross-team boundary holds.
FP-discriminators (consistent with the GitLab run -> cross-target stable)
/users/{id}/status -> IDOR-weak, NO marker. Presence (online/offline) is readable by any
authenticated user by design. Benign. (Same "global utility behind auth" class as GitLab /version.)
- inconclusive A=403/404 = alice herself can't reach it (admin-only channel
moderations /
member_counts_by_group / team channels/private, user tokens, file link; empty drafts).
Nothing to leak -> correctly NOT flagged. A!=200 baseline is the honest "can't assert ownership" bucket.
Honest coverage caveat (vs the GitLab run)
GitLab was FULL-surface (route-table dump, 1419 routes). Mattermost was a CURATED high-value CORE list
(39 endpoints across every object type's by-id read + cross-tenant write). The brutecat-class IDOR
pivots are all covered and all held, but this is NOT exhaustive enumeration. NEXT TICK (if revisited):
build full-surface enumeration from Mattermost's OpenAPI (api.mattermost.com spec is multi-file/built;
the published combined-yaml URLs 404'd this run - needs the repo build step or a per-path scrape).
Reflection (per Standing Reflection Mandate)
- WORKED: session-token-header auth recipe + first-user-admin bootstrap; marker-based read triage held
0-FP; cross-team boundary differential is the right model for Mattermost tenancy.
- DIDN'T (as a finding): no IDOR/BAC - Mattermost core authz holds. Expected for a mature product's core.
- UNEXPECTED: none material. FP classes matched GitLab (status/presence, admin-gated-to-A) -> the
FP-discriminator set is stabilizing across targets (bank toward a reusable triage allowlist).
- BANK -> this file + the cross-target FP-discriminator stability note.
Doctrine: double WALK CLEAN this session (GitLab + Mattermost). The compounding asset is the engine +
the now-3-target adapter set (GitLab full-surface, Mattermost core) + the stabilizing FP-discriminator
library. NO auto-submit. Nothing staged for the operator from this session (no confirmed hit).