drippa
Back to drippa

Security Hardening: Role Escalation Closed

tl;dr: We closed a hole that let org admins hand themselves owner, then spent two days scrubbing the rest of the attack surface.

Published Tuesday, August 11, 2026

A security researcher spent about ninety minutes prodding our production environment, spinning up and tearing down their own organizations. No customer tenant was touched and no isolation boundary broke. But they found something real: an internal route for adding members never checked which role you were asking for, so any org admin could mint an owner — themselves, or a second account they controlled. Owners can delete the organization, transfer ownership, and remove the actual owner. A contractor invited as an admin could have walked off with a customer's org.

We deleted the route rather than patching it. Nothing called it; the product has always used invitations, and invitations already refuse to hand out owner. That makes invitations the single path into an organization, with no second set of rules to drift out of sync. The role field is now a strict enum with no default, so the fail-open case can't quietly return. Along the way we fixed the things that ride shotgun with a route nobody was watching: deliberate not-found responses now return 404 instead of a 500 with a traceback attached, organization creation and invitation sending got rate limits they never had, and CORS traded its wildcards for explicit allowlists. Audit logs now survive the deletion of the organization they describe — previously, deleting an org erased the record of the deletion, which is exactly backwards.

The next two days went to the quieter stuff. File uploads and downloads built their backend URLs by hand, and the IDs in those URLs came from the browser unvalidated — a crafted value could steer a request down a backend path the browser shouldn't reach. Every path parameter is now checked against the UUID shape before it goes anywhere, which rejects slashes, dots, query strings and encoded traversal in one move. We also found a host check that matched on substring, meaning a URL from evil-github.com could pass itself off as GitHub and hand us an attacker-chosen repository. Now the hostname is compared exactly, and lookalikes fall back to trusted metadata like they always should have. Last, logs stopped recording things they had no business keeping: a code path was writing the first few characters of a valid API key, and Slack workspace names appeared in nine lines where the opaque team ID already did the job.

None of this shows up in the UI, and that's the intent. Your admins keep the permissions they're supposed to have, your audit trail outlives the org it belongs to, and the parts of the system that talk to the outside world are a good deal less credulous than they were last week.

Nothing here changes how the product feels day to day — that's the point. Thanks to the researcher who went poking; the invitation is open to anyone else who wants to.