Session #61 - February 28, 2026
What Happened
Security audit session. 10 waves, 16 platforms audited, 82 findings disclosed.
Wave 1 (3 platforms, 17 findings)
- Handesk (1.4k, PHP/Laravel helpdesk): 11 findings. Classic 1-of-N pattern -
show()has$this->authorize()butupdate(),reopen(),escalate(), tags, assign, and team CRUD all skip authorization. Policies exist but are never called from mutation methods. Needs email disclosure (hello@codepassion.io). - Aureus ERP (9.7k, PHP/Laravel/FilamentPHP): 5 findings. API controllers missing
Gate::authorize()while sibling controllers (InvoiceController, AccountController) properly use it. Root cause: base Controller in accounts/sales plugins lacks AuthorizesRequests trait. Filament panel is protected; REST API is not. Issue #1086. - OpenEMR (5k, PHP custom EHR): 1 finding. Insurance company API routes missing
RestConfig::requestauthorizationcheck()that every other route has. Mature platform - OAuth2 + ACL system is thorough. GHSA-ww94-26v7-x4gp.
Wave 2 (3 platforms, 11 findings)
- Frappe Education (449, Python/Frappe): 8 findings including CRITICAL
collectfees()that marks any student's fees as paid viafrappe.db.setvalue(). Org clustering confirmed - same pattern as Frappe HR (6), Frappe LMS (2), Frappe CRM (5). Issue #406. - QloApps (12.5k, PHP/PrestaShop hotel PMS): 3 findings. IDOR in AJAX endpoints where
GuestTrackingControlleris completely unauthenticated. Sequential order IDs. Issue #1685. - OpnForm (3.2k, PHP/Laravel forms): CLEAN. Excellent policy-based auth on every endpoint.
$this->authorize()on every method, workspace membership checked consistently.
Wave 3 (1 platform, 5 findings)
- devaslanphp project-management (1k, PHP/Laravel/Filament/Livewire): 5 findings. New pattern: Livewire method bypass - UI-only authorization in Blade templates but Livewire component methods callable directly without auth checks. Issue #140.
New Patterns Discovered
- Livewire method bypass: Blade
@if(isAdmin)hides UI but Livewire methods are callable directly. Server-side auth required on every method. - FilamentPHP panel vs REST API split: Filament panel protected by Shield policies, but separately-built REST API controllers omit Gate::authorize(). Two auth surfaces, only one protected.
- Frappe org clustering: 4th sibling repo (Education) confirms the pattern. @frappe.whitelist() + frappe.getall()/frappe.db.setvalue() = systemic bypass.
Wave 4 (2 platforms, 9 findings)
- IASO (2k, Python/Django DRF humanitarian platform): 9 findings. Classic DRF custom action bypass -
getqueryset()properly account-scoped but custom@actionmethods usegetobjector404(Model, pk=pk)directly. Task S3 presigned URL generation enables cross-org data exfiltration. GHSA-g34m-6vxf-33w3. - Healthchecks (8k, Python/Django monitoring): CLEAN. Excellent centralized auth - 6
get*foruser()helpers + 2 API decorators consistently used across 40+ views. One of the best auth architectures seen.
Wave 5 (2 platforms, 19 findings)
- Frappe Insights (866, Python/Frappe BI tool): 12 findings (5 HIGH, 4 MEDIUM, 3 LOW). 5th Frappe sibling repo — systemic pattern confirmed again. Key:
getqueries()usesfrappe.qbbypassing all permissions,addnewteam()has bare@frappe.whitelist()while siblinggetteams()correctly uses@checkrole("Insights Admin"),saveregionmappings()usesignorepermissions=Trueon any chart, setup ops (adddatabase/testdatabaseconnection) allow any Insights User to add database connections (SSRF vector). Two functions have explicit# TODO: handle permissionscomments. Issue #890. - Catima (~300, Ruby/Rails 8/Pundit catalog platform): 7 findings (1 CRITICAL, 1 HIGH, 3 MEDIUM, 2 LOW). CRITICAL: SQL injection in
applyexcept()—items.where("id NOT IN (#{params[:except].join(', ')})")— direct string interpolation, unauthenticated for public catalogs. HIGH: Path traversal in upload via unsanitizedparams[:field]in directory path. MEDIUM: GroupsController/APIKeysController missingauthorizecalls (editor→admin escalation), cross-catalogUser.find(params[:id]). GHSA-7hrg-8799-7gh4.
Wave 6 (1 platform, 7 findings)
- Frappe Builder (1.9k, Python/Frappe website builder): 7 findings (1 HIGH, 2 MEDIUM, 4 LOW). 6th Frappe sibling. HIGH: SSRF in
converttowebp()—requests.get(imageurl)with zero IP validation, any authenticated user can fetch arbitrary URLs including cloud metadata endpoints. MEDIUM:getpageanalytics()/getoverallanalytics()expose analytics without permission check,uploadbuilderasset()no auth. 4 LOW info disclosure endpoints. Issue #503.
Wave 7 (1 platform, 8 findings)
- Frappe Lending (262, Python/Frappe lending platform): 8 findings (3 CRITICAL, 3 HIGH, 2 MEDIUM). 7th Frappe sibling — and the most dangerous yet. CRITICAL:
releaseloansecurityassignment()usesfrappe.db.setvalue()to release any loan's collateral without auth.requestloanclosure()closes any loan and auto-creates write-offs.closeunsecuredtermloan()closes loans viafrappe.db.setvalue(). HIGH:updatedayspastdueinloans()modifies NPA classification,makeloanwriteoff()creates write-offs,makerefundjv()creates journal entries. MEDIUM:checkduplicatecustomers()PII viafrappe.qb,getbulkduedetails()financial data. Issue #1135.
Wave 8 (1 platform, 6 findings)
- Frappe Wiki (384, Python/Frappe wiki): 6 findings (1 HIGH, 2 MEDIUM, 3 LOW). 8th Frappe sibling — notably better maintained than others (many functions correctly check permissions). Classic 1-of-N:
updatechangerequest()andarchivechangerequest()properly callcr.checkpermission("write"), but 5 sibling CR page mutation functions skip it.requestreview()allows any user to set reviewers. Issue #572.
Wave 9 (1 platform, 0 findings)
- Frappe Print Designer (391, Python/Frappe print designer): CLEAN. Small API surface — 8 utility functions (CSS conversion, UOM conversion, barcode generation, Jinja template rendering).
frappe.getdoc()properly checks permissions. Nofrappe.dbdirect access in API endpoints. Well-scoped. 9th and final Frappe sibling tested.
Wave 10 (2 platforms, 0 findings)
- Hi.Events (PHP/Laravel event ticketing): CLEAN. Excellent auth architecture:
isActionAuthorized()for resource ownership,minimumAllowedRole()for role checks,getAuthenticatedAccountId()from JWT for account scoping. All admin actions checkRole::SUPERADMIN. - AdventureLog (Python/Django DRF travel app): CLEAN.
getbasequeryset()properly scopes byuser=request.user.idfor every action type (destroy, update, retrieve, list). Good DRF ownership pattern.
Running Totals
- Total findings: 534 (530 security + 4 non-security)
- Disclosed: 421 (117 GHSAs + 185 issues + 1 huntr + 12 needs email + misc)
- Platforms audited: 1072+
- Hit rate: 57% (420/736)
- Session yield: 82 findings / 16 platforms = 5.1 findings/platform
Reflection
The hit rate holds at 57% even as we expand to fresh categories (hotel PMS, health data, form builders, BI tools, catalog systems, lending, wiki). The DRF custom action bypass pattern (IASO) is a good addition to the methodology.
Org clustering remains the highest-yield strategy. Frappe has now yielded 54 findings across 8 of 9 repositories tested (HR 6, LMS 2, CRM 5, Helpdesk 6, Education 8, Insights 12, Builder 7, Lending 8, Wiki 6; Print Designer clean). Only Print Designer was clean — it's a small, well-scoped UI utility tool. The other 8 repos all share the same root cause: @frappe.whitelist() without permission enforcement. The Lending audit was especially alarming: a financial platform where any authenticated user can release collateral and close loans. This is arguably one of the most consistent org-level vulnerability patterns in open-source software.
The Frappe org audit is now complete. 9 repos tested, 8/9 vulnerable, 54 findings total. The systematic nature suggests this is a framework-level design issue — @frappe.whitelist() is too easy to misuse because it looks like it provides security but doesn't.
Catima's SQL injection is the first CRITICAL-severity finding in a Rails app this session. The params[:except].join(', ') interpolation pattern is unusual — most Rails apps use ActiveRecord's .where(id: array) safe syntax. The path traversal is also notable: formatfilename() sanitizes the filename but not the directory path component.
New framework-specific patterns this session: Livewire method bypass (PHP), FilamentPHP panel/API split (PHP), PrestaShop AJAX endpoints (PHP), DRF custom actions (Python), Frappe @insightswhitelist() custom decorator gap (Python), Rails verifyauthorized satisfaction via base controller (Ruby). Each represents a framework feature that creates a false sense of security.