Root cause
Authentication & access control introduced
A Claude-assisted change adds order read and update endpoints that look orders up by ID without a company check.
How AI contributed: Direct introductionPublished Jul 16, 2026 in
roskus/prospero-flow-crmAll 3 findings in this repository
Provisional finding. Full causal and release verification remains incomplete.
Prospero Flow CRM's new order and order-item API looked records up by ID across all companies and never checked that the record belonged to the caller's company, although the neighbouring list and delete handlers did. Any authenticated API user could read another company's order and its items, and users with order permissions could change another company's order or read, change or delete its items. A Claude-assisted change added the order read and update handlers this way, and a later change added the item handlers with the same lookup. The fix, three days later, scopes all five lookups to the caller's company. The vulnerable code was never in a tagged release: the release before it has no API and the next one already contains the fix, so the advisory's version range overstates what shipped.
Root cause
A Claude-assisted change adds order read and update endpoints that look orders up by ID without a company check.
Fix
The repair scopes the order and order-item lookups to the caller's company.
Case facts
@@ -0,0 +1,42 @@+<?php++declare(strict_types=1);++namespace App\Http\Controllers\Api\Order;++use App\Models\Order;+use Illuminate\Http\JsonResponse;+use OpenApi\Attributes as OAT;++class OrderReadController+{+ #[OAT\Get(+ path: '/order/{id}',+ summary: 'Get Order details',+ security: [['bearerAuth' => []]],+ tags: ['Order'],+ parameters: [+ new OAT\Parameter(+ name: 'id',+ in: 'path',+ required: true,+ description: 'Order ID',+ schema: new OAT\Schema(type: 'integer')+ ),+ ],+ responses: [+ new OAT\Response(response: 200, description: 'Order found'),+ new OAT\Response(response: 404, description: 'Order not found'),+ ]+ )]+ public function read(int $id): JsonResponse+ {+ $order = Order::find($id);++ if (! $order) {+ return response()->json(['message' => 'Order not found'], 404);+ }++ return response()->json(['order' => $order], 200);+ }+}@@ -0,0 +1,55 @@+<?php++declare(strict_types=1);++namespace App\Http\Controllers\Api\Order;++use App\Http\Requests\OrderUpdateRequest;+use App\Models\Order;+use Illuminate\Http\JsonResponse;+use Illuminate\Support\Facades\Auth;+use OpenApi\Attributes as OAT;++class OrderUpdateController+{+ #[OAT\Put(+ path: '/order/{id}',+ summary: 'Update an Order',+ security: [['bearerAuth' => []]],+ tags: ['Order'],+ parameters: [+ new OAT\Parameter(+ name: 'id',+ in: 'path',+ required: true,+ description: 'Order ID',+ schema: new OAT\Schema(type: 'integer')+ ),+ ],+ requestBody: new OAT\RequestBody(+ required: true,+ content: new OAT\JsonContent(ref: '#/components/schemas/Order')+ ),+ responses: [+ new OAT\Response(response: 200, description: 'Order updated successfully'),+ new OAT\Response(response: 403, description: 'Unauthorized'),+ new OAT\Response(response: 404, description: 'Order not found'),+ new OAT\Response(response: 422, description: 'Validation failed'),+ ]+ )]+ public function update(OrderUpdateRequest $request, int $id): JsonResponse+ {+ $order = Order::find($id);++ if (! $order) {+ return response()->json(['message' => 'Order not found'], 404);+ }++ $data = $request->validated();+ $data['updated_by'] = Auth::user()->id;++ $order->update($data);++ return response()->json(['order' => $order], 200);+ }+}@@ -31,7 +32,9 @@ class OrderReadController )] public function read(int $id): JsonResponse {- $order = Order::with('items')->find($id);+ $order = Order::where('company_id', Auth::user()->company_id)+ ->with('items')+ ->find($id); if (! $order) { return response()->json(['message' => 'Order not found'], 404);@@ -39,7 +39,7 @@ class OrderUpdateController )] public function update(OrderUpdateRequest $request, int $id): JsonResponse {- $order = Order::find($id);+ $order = Order::where('company_id', Auth::user()->company_id)->find($id); if (! $order) { return response()->json(['message' => 'Order not found'], 404);@@ -31,7 +32,9 @@ class OrderItemReadController )] public function read(int $id): JsonResponse {- $item = Item::find($id);+ $item = Item::whereHas('order', function ($query) {+ $query->where('company_id', Auth::user()->company_id);+ })->find($id); if (! $item) { return response()->json(['message' => 'Item not found'], 404);AI-assisted change 4f2e22a1a24b346894232301123bd0b13ca3ae2ec891debe983e1bd570b1d5b6 · Fix 9f1aa943aa2f393335fe7a4687b0826c4c3cbf1e6cbd111eebb0536db42cc332