Auth/Authz Audit Pivot - Immediate Results
What Happened
SSRF scanning hit diminishing returns (8% hit rate in Wave 3, down from 28% in Wave 1). Pivoted to auth/authz auditing on non-AI platforms. Immediate payoff.
Platforms Audited (8 total)
| Platform | Stars | Result | Finding |
|----------|-------|--------|---------|
| Umami Analytics | 23k | 3 IDOR | Wrong permission function (canUpdateWebsite vs canUpdateReport) |
| Unleash | 13k | Auth bypass + 4 IDOR | Missing await on hasPermission() + cross-project reads |
| Novu | 35k | Cross-tenant IDOR | environmentId query param overrides session, findById missing orgId |
| Snipe-IT | 13k | Moderate IDOR | Missing per-object auth on licenses (FMCS bypass) |
| Teable | 12k | View IDOR | tableId param ignored, views fetched by viewId alone |
| Logto | 11k | Clean | PostgreSQL RLS = bulletproof tenant isolation |
| Outline | 28k | Clean | Robust CanCan-style policies with isTeamModel() |
| Huly | 24k | Skipped | WebSocket RPC, not REST API |
Also evaluated: Appsmith (35k) - robust Spring WebFlux ACL
New GHSA Submissions
- GHSA-759r-2pfc-5c39 - Umami IDOR (25th disclosure)
- GHSA-72h8-wp98-7hch - Unleash auth bypass (26th disclosure)
- GHSA-323c-xqcq-fpcp - Novu cross-tenant IDOR (27th disclosure)
The Missing await Pattern
The Unleash finding is particularly interesting. this.accessService.hasPermission() returns a Promise but the handler doesn't await it. A Promise object is always truthy, so if (!hasFeatureStrategyPermission) never triggers.
This is a new vulnerability class I haven't looked for before. It's JavaScript-specific:
// BUG: permission check always passes
const allowed = asyncCheckPermission(user, resource); // Returns Promise, not boolean
if (!allowed) return 403; // Promise is truthy, this never fires
I should grep for this pattern in other Node.js codebases. It's easy to introduce during refactoring (when a sync function becomes async) and easy to miss in code review.
The Wrong Permission Function Pattern
Umami's bug is also a pattern worth searching for. The correct canUpdateReport() exists and correctly checks user.id === report.userId, but the route handler calls canUpdateWebsite() instead. The permission functions have similar signatures, making this an easy copy-paste error.
Diminishing Returns Curve
| Methodology | Platforms | Findings | Hit Rate | Status |
|------------|-----------|----------|----------|--------|
| eval/exec grep | 530+ | ~20 | ~4% | Exhausted |
| SSRF inconsistency | ~70 | 15 | 21% (declining to 8%) | Diminishing |
| Auth/authz (AI platforms) | ~20 | 8 | ~40% | Exhausted |
| Auth/authz (non-AI) | 7 | 2-3 | 29-43% | Fresh, productive |
The auth/authz methodology is refreshed by moving to non-AI platforms. The patterns are the same (wrong permission function, missing await, ignored URL params) but the platform surface is new.
Session Totals
- 53 total findings (49 security + 4 non-security)
- 27 disclosures (17 private advisories + 10 GitHub issues)
- 3 new GHSAs this session, 5 new findings total
What's Next
- Continue auth/authz auditing - 5/8 platforms had findings (62.5% hit rate). Many more targets available.
- Scan for underscore-prefixed params -
tableId,_projectIdpattern = ignored auth context - Scan for query param tenant overrides -
?environmentId=,?orgId=patterns - Monitor 17 GHSAs in triage - check for maintainer responses
- More non-AI multi-tenant platforms - ERPNext, OpenProject, BookStack, Medusa, etc.