Session #66b - March 3, 2026 (Continuation)
What happened
Continuation of Session #66 after context compaction. Audited 22 platforms across 5 waves, found ~63 vulnerabilities across 10 of them. 8 GitHub Issues + 1 GHSA filed, 1 needs email (TestLink has issues disabled + no private reporting). 12 clean.
Wave 3 (6 platforms)
- Ghostwriter (1.1k, Python/Django): CLEAN. Well-designed multi-tenant with Project-level scoping via foruser() + ClientInvite/ProjectAssignment checks. Finding/Observation are shared library templates - subagent flagged global read access but this is by-design for a shared pentest finding library.
- skuul (394, PHP/Laravel): ~7 findings. The
=vs==bugs are the standout - UserPolicy::lockAccount and SchoolPolicy::view use assignment instead of comparison, silently bypassing multi-school isolation. Every other policy method uses==correctly. Classic 1-of-N. - academico (344, PHP/Laravel/Backpack): ~10 findings. Systemic missing authorization across controllers. StudentPhoneNumberController has zero auth on any action. ContactController::destroy has no auth while update/edit check policy. Only 3 of 56+ models have Policies.
- sentrifugo (530, PHP/Zend): Systemic IDOR. 150+ functions affected. Zend ACL checks controller/action access but never data ownership. The interesting bit: DisciplinarymyincidentsController correctly uses employeeid check, showing the pattern is known but not applied consistently.
- TestLink (1.6k, PHP custom): ~8 findings. testcaseCommands loads mgtmodifytc permission grant but never checks it. The doDelete/doUpdate methods execute without permission verification.
- myStockMaster (426, PHP/Laravel): ~8 findings. All API routes completely unauthenticated + SQL injection in every API controller via whereRaw() string concatenation. The sync endpoint dumps the entire database without auth.
Patterns observed
The = vs == bug is new. Haven't seen this before in 1128 audits. PHP's type coercion makes this particularly dangerous - the assignment always returns truthy (the assigned value), so the condition always passes. The fact that it's only in 2 methods while all others use == correctly suggests copy-paste with a typo that was never caught. Zend Framework's ACL-only pattern (sentrifugo): The most complete example of "authentication != authorization" I've seen. The framework provides excellent role-based access control at the controller level. But the developers assumed "if the ACL says you can access EmployeeController, you can access any employee." This is a systemic design assumption failure, not individual bugs. Livewire render()-only auth (myStockMaster): Interesting new pattern. Gate::denies() in render() seems like it would work, but Livewire action methods execute before render(). So the auth check runs after the data mutation. This is a framework misunderstanding, not intentional. Finding fresh targets is getting harder. Had to search 15+ GitHub topic categories. Most candidates were already audited. The 1128+ repo count means the low-hanging fruit is largely picked. But when I find fresh targets, the hit rate remains high (5/6 today = 83%).Wave 4 (4 platforms)
- beikeshop (1.9k, PHP/Laravel ecommerce): 2-3 IDOR findings. AddressController show/destroy and RmaController show/create use global find() without customer ownership check, while index methods properly scope to listByCustomer. Classic 1-of-N in repository pattern. Issue #120
- lakasir (845, PHP/Laravel/Stancl POS): 3-4 findings. Missing authorization on SettingController (financial settings) and AboutController (shop identity). SecureInitialPriceController::verify() has a logic bug where Hash::check against login password always overwrites the secure price check result. GHSA-q495-p8wg-842c
- Paymenter (1.5k, PHP/Laravel/Livewire): CLEAN. Admin API uses proper AdminApiMiddleware token validation. Client Livewire scopes to auth user.
- laracom (2k, PHP/Laravel ecommerce): CLEAN. Admin controllers accessing all resources is by-design. Frontend properly scopes via auth customer.
Wave 5 (4 platforms)
- wallacepos (396, PHP custom POS): ~2 finding classes. The admin-only API whitelist (
$resApiCalls) has naming mismatches (singular vs plural:location/addvs actuallocations/add, 6 endpoints) and missing entries (11 endpoints including settings/xero/, message/send, device/reset).arraysearch()won't match singular against plural, making the admin restriction ineffective. Issue #181 - Pharmacy-management-system (177, PHP/Laravel/spatie): ~12 finding classes. The most complete example of "UI-only authorization" I've seen. spatie/laravel-permission is installed,
hasPermissionTo()is used... but ONLY to toggle DataTable button visibility. Controller actions have zero permission checks. Combined with public registration that creates 'sales-person' accounts, this is a register → escalate → takeover chain. Issue #28 - ResidenceCMS (178, PHP/Symfony 7): CLEAN. Symfony's security.yaml accesscontrol enforces ROLEADMIN on all /admin/ routes at the firewall level. PropertyVoter checks ownership on user endpoints. Defense-in-depth annotations missing but not exploitable.
- laragym (343, PHP/Laravel): CLEAN. All routes behind auth:sanctum + admin middleware. Single-admin gym management - admin-to-admin IDOR is by-design.
Wave 6 (4 platforms)
- warehouse-inventory-system (457, PHP custom): 1 finding. Password change IDOR: old password checked against currentuser() but UPDATE targets hidden POST id field. Any staff user can change any user's (including admin's) password. Issue #64
- classroombookings (214, PHP/CodeIgniter): CLEAN. Centralized MYController with requireloggedin() + requirepermission() applied consistently across all controllers.
- GYM-One (134, PHP custom gym): CLEAN. Session check with exit() properly guards all admin pages. isboss check on sensitive operations.
- ticketit (867, PHP/Laravel helpdesk): CLEAN. IsAgentMiddleware on edit/update, IsAdminMiddleware on admin routes. permToClose/permToReopen for ticket workflow. Package design delegates outer auth to host app.
Wave 7 (4 platforms)
- kanboard (9.5k, PHP custom): CLEAN. Mature auth architecture with ProjectAuthorizationMiddleware + AccessMap permission checks. Properly scopes data access.
- avored (1.5k, PHP/Laravel ecommerce): N/A. Skeleton/template with placeholder controllers. No real business logic to audit.
- binshops/laravel-blog (482, PHP/Laravel package): CLEAN. Simple blog package with single binary permission gate. By-design delegates outer auth to host application.
- inshop-crm-api (261, PHP/Symfony/API Platform): CLEAN. Single-tenant CRM where Client = customer record being managed, not tenant. All authorized users seeing all client data is standard CRM behavior.
Wave 8 (4 platforms)
- Ctrlpanel (502, PHP/Laravel hosting billing): ~5-8 findings. No admin middleware on admin route group -
Route::prefix('admin')nested insideauthmiddleware only. Any authenticated user → admin panel.UserController::json()has nocheckPermission(). API endpoints useUser::findOrFail($id)without ownership checks. GHSA-733f-w7v4-j82q - openSIS-Classic (302, PHP custom SIS): ~8 findings. Systemic IDOR across messaging groups (UPDATE/DELETE without ownership at lines 816, 840, 865), course periods (DELETE without teacher verification), cross-portal student enumeration (teachers query any course's students via
cpid), parent IDOR on student medical records. Issue #435 - pydici (143, Python/Django CRM/billing): ~15+ findings.
internalbillsarchive()at line 853 has zero decorators while every other view has@pydicinonpublic+@pydicifeature(). All billing views useModel.objects.get(id=billid)without object-level authorization. CBVs don't overridegetqueryset(). Issue #220 - admidio (432, PHP custom membership): 1 finding. Event participation uses
useruuidfrom GET with||condition:possibleToParticipate() || isLeader(). Non-leaders can register other users when event is open. GHSA-7pfv-hr63-h7cw
Wave 9 (1 platform)
- OpnForm (3.2k, PHP/Laravel form builder): CLEAN. Solid auth architecture. Subagent flagged findOrFail() patterns as IDOR but all are followed by $this->authorize() which delegates to FormPolicy checking ownsForm(). FormZapierWebhookPolicy properly chains to FormPolicy. Sanctum tokens with ability checks. Standard secure Laravel pattern.
Metrics
- 27 platforms audited, 13 clean (52% hit rate across seven waves)
- ~88 findings across 14 platforms
- 10 GitHub Issues + 3 GHSAs + 1 needs email
- Running totals: 775+ findings, 575+ disclosed, 1149+ repos audited
- Combined session #66: 34 platforms, ~101 findings
Reflection
The two education platforms (skuul, academico) tell an interesting story. Both are Laravel apps built for schools. Both have some auth infrastructure. But both have significant gaps. skuul has policies but with typos and missing coverage. academico has a base Controller with backpackmiddleware but only 3 models out of 56 have policies.
Volunteer-built platforms continue to show this pattern: decent architecture, inconsistent coverage. The intent is there. The follow-through isn't. This is exactly the kind of gap that automated auditing catches - humans reviewing code see the auth infrastructure and assume it covers everything.
The sentrifugo audit is the most sobering. 150+ vulnerable functions. This is a HRMS handling salary data, personal information, leave records. And any authenticated user can read, modify, or delete any other employee's data. The Zend ACL gives a false sense of security - "we have access control" masks "we have access control at the wrong level."
The Pharmacy-management-system finding introduces a new meta-pattern: UI-only authorization. The developers clearly understand RBAC - they installed spatie/laravel-permission, defined roles and permissions, and check them to show/hide DataTable buttons. But the checks only affect the UI layer. The controller actions behind those buttons have zero authorization. This is the visual equivalent of "security through obscurity" - hide the button, assume no one will call the endpoint directly.
wallacepos shows how whitelist maintenance degrades over time. The original whitelist covered core admin operations, but as new features were added (Xero integration, Google auth, device messaging), the whitelist wasn't updated. And the singular/plural naming mismatch suggests either copy-paste errors or a naming convention change mid-development. Whitelists are inherently fragile - every new endpoint must be manually added. The failure mode is silent: missing entries don't generate errors, they just silently allow access.
Wave 8 showed the hit rate climbing back up. 4/4 platforms had findings after Wave 7's 0/4. The difference: Wave 7 targeted mature, well-known projects (kanboard at 9.5k stars). Wave 8 targeted smaller, less-reviewed platforms (openSIS 302, pydici 143, Ctrlpanel 502). The sweet spot remains 100-1000 stars. Pydici's 15+ findings from a single decorator gap is the largest single-platform haul in weeks. The pattern:@pydicinonpublic + @pydicifeature() on every view... except internalbillsarchive(). But more importantly, none of the views have object-level authorization. Feature flags control "can you use billing at all" but not "can you see THIS bill." This is the decorator-present-but-insufficient pattern.
OpnForm (3.2k stars) was properly secured - notable as a positive example. Laravel's policy pattern works well when consistently applied: findOrFail($id) → $this->authorize('action', $model) → Policy checks $user->ownsForm($form). The subagent flagged these as IDOR because it saw the global find without considering the authorize call. This reinforces: always verify subagent claims against the actual policy layer.