Research/plausible-analytics-leftovers
Series0-dayNo CVEHighPublic

Plausible Analytics leftovers

Plausible Analytics master d44e104ae3943a still lets a guest editor read a pending ownership-transfer nanoid from People HTML, then register and accept. PUT traffic-change-notification still casts site_id, so an editor of their own site re-parents spike notify onto a victim. Loopback labs.

Name
Plausible Analytics leftovers
Series
Plausible Analytics leftovers
Phase
0 · The pin
Type
0-day analysis
CVE
n/a
CVE Risk
high
Disclosure Status
public
Vendor
Plausible
Affected
Plausible Analytics GitHub master d44e104ae3943a9240d5cb6b62e67dff0f069d3a (2026-10-16). Last CE tag v3.2.1 is five months behind. SECURITY.md tells operators to run :latest.
Published
17 Oct 2026
Updated
17 Oct 2026
Tags
0-day, plausible, privilege-leftover, idor, leftover, remote

Two leftovers on the tree operators are told to run

A named advisory is a fence. The leftover is whatever still walks around it.

I am abraxas. This is the series hub for two leftovers I labbed on Plausible Analytics GitHub master d44e104ae3943a9240d5cb6b62e67dff0f069d3a (2026-10-16), posted as @abraxas_null. Community edition. MIX_ENV=ce compiles lib/. extra/lib is EE/cloud and stayed out of these labs. Loopback witness labs. No shells.

SECURITY.md patches only latest MAJOR.MINOR and tells operators to run plausible/analytics:latest. Last CE tag v3.2.1 (e4f5a87a5570bd61605217e7ceb376636db8eddb, 2026-05-15) is five months behind this pin. A leftover on HEAD is a leftover on the image people actually pull.

whoareme Bug 02 (disclosed 2026-05-22) closed invite-only LiveView register without a valid invite, plus Shield / Goal site_id mass-assignment. Storybook RCE (CVE-2026-8467 / GHSA-55hg-8qxv-qj4p) left with /storybook. The People page still prints the invite bearer in an HTML id=. Traffic-change notification still casts :site_id.

The leftover I would patch first is the editor invite. Default CE is DISABLE_REGISTRATION=invite_only and ENABLE_EMAIL_VERIFICATION=false. A guest editor reads a pending ownership-transfer nanoid, registers as the invitee, accepts. The site moves.

The two posts, in that order:

  1. plausible-editor-invite - People HTML leaks transfer nanoid to editors
  2. plausible-traffic-notify - PUT traffic-change-notification site_id leftover of Shield/Goal

Vendor intake is security@plausible.io. Unpaid VDP: plausible.io/vulnerability-disclosure-program. I am not opening a public issue on plausible/analytics. Packs are AGPLv3 under github.com/abraxas. Labs talk to loopback and print a witness.

Why this product

Plausible is the privacy-respecting analytics box people self-host because they do not want Google. Sites live on teams. An editor is supposed to edit the site, not steal it. Traffic-spike mail is supposed to go to people who already own that site.

I wanted unpublished Critical or High on the tree operators are told to run. Remote. Authenticated is fine when the role is editor. Medium stays on the floor.

The security model is honest once you look at it. Phoenix AuthorizeSiteAccess roles: owner / admin / editor / super_admin. Guest editor maps to :editor. Guest viewer does not. Nanoid 21-char invitation_id / transfer_id is the register-from-invite bearer. Login rate limit 5/min/IP on CE. First launch allows the first user. That is the box.

I needed the HTML id= that still holds a bearer, and the changeset whoareme missed.

How the pin was chosen

GitHub master HEAD on the day I cloned. Commit d44e104ae3943a9240d5cb6b62e67dff0f069d3a, 2026-10-16. I did not pin v3.2.1. SECURITY.md says :latest. Last CE tag is five months old. A leftover on a stale tag is a history lesson. A leftover on HEAD is current.

Labs build plausible-lab:d44e104ae3943a from that SHA with MIX_ENV=ce. postgres:16-alpine. clickhouse/clickhouse-server:24.12-alpine. MAILER_ADAPTER=Bamboo.LocalAdapter. Replay is ./run.sh in each pack. First run clones the pin if the image is missing.

The skip list is half the hunt

Before you look for leftovers, you write down what is already public so you do not "discover" a CVE with a press release. I did not re-file:

  • CVE-2026-8467 / GHSA-55hg-8qxv-qj4p / GHSA-mhcv-h7gf-57cf storybook RCE. Fixed v3.2.1 by removing /storybook. Endpoint stays gone on this pin.
  • whoareme 2026-08-25 Bug 02 (disclosed 2026-05-22): invite-only LiveView register without a valid invite; save-hostname-rule / save-ip-rule / save-page-rule / save-country-rule / save-goal site_id mass-assignment; stored XSS in goal display_name. Patched on this pin (Goal.changeset no longer casts :site_id; Shield struct(site_id:) from assigns; check_registration_enabled(:default) vs :invitation). The leftover of that mass-assignment pass is traffic-change notification. The leftover of treating the invite nanoid as a secret is People HTML.
  • whoareme Bug 03 (disclosed 2026-06-01, EE): SSO domain-verification SSRF to ClickHouse then Postgres COPY TO PROGRAM. Plausible.SSRF.get/2 on this pin. EE. Out of the CE labs.

Hunts that returned nothing in this bar: LiveView CSRF (check_origin true; prod.exs false does not win over runtime), Stats API, ingest, shared links, plugins MCP (POST /mcp 501), S3, 2FA, password reset (BASE_URL not Host), tracker XSS (Jason.encode!), ClickHouse (always AND site_id), super-admin debug, CSV import, funnels nested goal_id (dangling FK recon of conversion names, not High), TeamSetup guest rename of unsetup personal team (Medium config), Installation TrackerScriptConfiguration cast :site_id (ingest is already unauth POST /api/event).

Two leftovers, one pin

# Pack Reach Class What an attacker can do
1 plausible-editor-invite Remote (guest/team editor) Privilege leftover GET /:domain/settings/people emits invitation-<nanoid>. Register as the invitee. Accept a pending ownership transfer. Site team_id moves.
2 plausible-traffic-notify Remote (owner/admin/editor of attacker site) IDOR POST .../enable on own domain, then PUT traffic_change_notification[site_id]=victim_pk. Spike/drop row re-parents. Oban mails attacker about victim traffic.

The editor invite is first because default CE makes the nanoid a register-and-accept path. Email verification is off. Invite-only still allows :invitation. A pending owner transfer is a site steal.

Traffic notify is the Shield/Goal sibling. Enable stamps assigns. Update takes the client map. Unique (site_id, type). Default install has no row, so the plant works. Mail recon is inferred from the notifier. The row move is lab-proven.

How I labbed them

Two independent compose projects. Unique loopback ports. Shared image tag plausible-lab:d44e104ae3943a. Witness strings in HTML ids and a Postgres site_id column. Stacks torn down.

  • Editor invite: 127.0.0.1:18180. DISABLE_REGISTRATION=invite_only. Editor people leaked Hc8DiBC2q08mv5LFmBysu. Viewer 404. Register overwrote posted email. Editor accept did not move team_id. Newowner accept moved sites.team_id 1 → 2.
  • Traffic notify: 127.0.0.1:18183. DISABLE_REGISTRATION=false so two teams can register. victim.lab id 1 team 1, attacker.lab id 2 team 2. Attacker not a member of victim. PUT victim path domain 404. PUT own domain with site_id=1 moved spike_notifications row 1, recipients stayed {attacker@lab.invalid}.

Replay: clone the pack, python3 *-Abraxas-Labs.py or cd lab && ./run.sh.

Contact

abraxaslabs.tech · github.com/abraxas · @abraxas_null · abraxas.null@proton.me