Date: 2026-06-23 Target: Discourse (bitnamilegacy/discourse:latest), self-hosted docker, disposable Tool: fuzzer-arm v0.3 (new adapter targets/discourse.py + run_discourse.py + run_discourse_curated.py) Cash-gate: Discourse runs a PAYING HackerOne bounty (bounty table) + source is 100% OSS & explicitly in-scope -> a source-reproducible IDOR on a self-hosted instance is cash-eligible (GitLab posture).
Api-Username header.
Each Identity carries token=None + extra_headers={Api-Key, Api-Username, Accept:json}; the
differential swaps Api-Username to replay alice's request as bob. Discourse's guardian enforces
that user's permissions -> marker in bob's response = cross-tenant IDOR.bin/rails binstub is BROKEN -> run rails via bundle exec ruby <file> after
require "config/environment" with PATH=/opt/bitnami/ruby/bin + RAILS_ENV=production (a login
bash -lc RESETS env and drops DISCOURSE_* vars -> use bash -c).Rails.application.routes.routes (1005 routes) since the binstub is broken.Focused re-run (run_discourse_curated.py) over the 24 high-value pivots with VALID A=200 baselines: every private pivot A=200 (alice) / B=403-or-404 (bob): /t/{id}, /raw/{id}, /t/{id}/posts, /posts/{id}, /posts/{id}/raw, /c/{id}/show (private category), PM topic+post, /topics/private-messages/{user}, /u/{user}/emails. Write-BAC (bob DELETE alice's topic/post/PM) -> B=403/422 (blocked). Benign non-flags: public profile/group/summary (anon=200, no marker). FLAGGED=0.
The FIRST full run (932 endpoints, no throttle) was INVALIDATED by Discourse's rate limiter:
alice herself got A=429 on most private pivots -> 506/932 inconclusive (no clean ownership
baseline). A flood-fuzz against a rate-limited target produces A!=200 and a FALSE "clean" (you never
actually tested the pivot). Discourse's max_reqs_per_ip_mode/max_admin_api_reqs_per_minute are
BOOT-TIME GlobalSettings read from the conf file (NOT the DISCOURSE_* env -> setting env had no
effect; flipping them needs a ~10min asset re-precompile). FIX THAT WORKED: provision -> redis-cli
FLUSHALL (resets the sliding-window counters) -> probe ONLY the curated pivots, throttled 0.3s ->
72 calls fit under the ~100/min budget -> clean baselines. RULE: on a rate-limited target, do NOT
blast the full surface; run a FOCUSED throttled probe of the high-value pivots, and treat a high
A!=200 (esp A=429) inconclusive count as "coverage failed", not "target clean".
anon-succeeds-on-write -> classify public-ok (benign), NOT BAC. The first Discourse run flagged 8
"BAC" that were PUBLIC-by-design writes (anon=200): inbound email webhooks (mailjet/postmark/sendgrid/
sparkpost), signup (/users,/u), public /categories/search, /pageview analytics beacon. The status-based
write heuristic must check anon FIRST: if anon can do it too, it's public, not cross-tenant BAC.
Validated: vulnapp positive control still 8/8 (its planted writes have anon=401 -> still flag).
This + the earlier follow/star + /self|/me|/current allowlist = the write-pass FP set is now tight.
Session net: GitLab + Mattermost + Discourse = 3 cash-gated blue-chips, ALL swept clean. The fresh tier is credit-only. Stop-condition met -> strategic readout to operator. NO auto-submit.