Root cause
Authentication & access control introduced
The assistant-written controller for a single transaction looked the record up by path id with no company filter and no permission request.
AI-assisted change: Claude
How AI contributed: Direct introductionPublished Aug 21, 2026 in
roskus/prospero-flow-crmAll 3 findings in this repository
The change added a full set of transaction endpoints written with a coding assistant. The listing endpoint scoped every query to the caller's company, but the single-transaction endpoint kept a raw primary-key lookup, so the company scope and the permission check that the sibling endpoints received were missing from exactly one handler. The route was protected only by token authentication, which proves who is calling, not which records that caller may see. An attacker holding any valid account could walk the id space and read other companies' financial records. The follow-up change adds a permission-checking request class and rewrites the lookup to filter on the caller's company id before the record is returned.
The route for a single transaction sat behind token authentication only, and the handler resolved the path id straight into a database lookup. Any signed-in caller from another company could therefore read a transaction by editing the id, because the query carried no company filter and the endpoint skipped the permission request class that the other transaction endpoints used.
Root cause
The assistant-written controller for a single transaction looked the record up by path id with no company filter and no permission request.
AI-assisted change: Claude
Fix
A permission-checking request class and a company-scoped query now gate the endpoint, so a caller can only read records that belong to their own company.
Fix by Gustavo Novaro · no AI marker found
Case facts
@@ -0,0 +1,42 @@+<?php++declare(strict_types=1);++namespace App\Http\Controllers\Api\Transaction;++use App\Models\Transaction;+use Illuminate\Http\JsonResponse;+use OpenApi\Attributes as OAT;++class TransactionReadController+{+ #[OAT\Get(+ path: '/transaction/{id}',+ summary: 'Get Transaction details',+ security: [['bearerAuth' => []]],+ tags: ['Transaction'],+ parameters: [+ new OAT\Parameter(+ name: 'id',+ in: 'path',+ required: true,+ description: 'Transaction ID',+ schema: new OAT\Schema(type: 'integer')+ ),+ ],+ responses: [+ new OAT\Response(response: 200, description: 'Transaction found'),+ new OAT\Response(response: 404, description: 'Transaction not found'),+ ]+ )]+ public function read(int $id): JsonResponse+ {+ $transaction = Transaction::find($id);++ if (! $transaction) {+ return response()->json(['message' => 'Transaction not found'], 404);+ }++ return response()->json(['transaction' => $transaction], 200);+ }+}@@ -4,8 +4,10 @@ namespace App\Http\Controllers\Api\Transaction; +use App\Http\Requests\TransactionReadRequest; use App\Models\Transaction; use Illuminate\Http\JsonResponse;+use Illuminate\Support\Facades\Auth; use OpenApi\Attributes as OAT; class TransactionReadController@@ -29,9 +31,9 @@ class TransactionReadController new OAT\Response(response: 404, description: 'Transaction not found'), ] )]- public function read(int $id): JsonResponse+ public function read(TransactionReadRequest $request, int $id): JsonResponse {- $transaction = Transaction::find($id);+ $transaction = Transaction::where('company_id', Auth::user()->company_id)->find($id); if (! $transaction) { return response()->json(['message' => 'Transaction not found'], 404);AI-assisted change 6684c8f38a7d15e121247e1b15eaa6b8f6cf99dab4a7095ea77dd261fcb3c7d3 · Fix b05d23a4f8cd9a87262cfb2021f1f321f4e69719be7274cf591bc15bd2efa438