Broken Authentication

Broken Authentication in Laravel: CVE-2026-35584 [Apr 2026] [CVE-2026-35584]

[Fixed Apr 2026] Updated CVE-2026-35584

Overview

FreeScout, built on Laravel, exposed an unauthenticated GET endpoint at /thread/read/{conversation_id}/{thread_id} prior to 1.8.212. Because the endpoint did not require authentication and did not validate that the provided thread_id belonged to the given conversation_id, an attacker could mark any thread as read without credentials and even observe whether a thread ID was valid via 200 vs 404 responses. This enabled IDOR-like behavior and potential tampering with opened_at timestamps across conversations. The impact spans confidentiality and integrity of user state and can enable abuse of read-tracking data across conversations, which aligns with CWE-306 (Missing Authentication for Critical Function) and CWE-639 (Authorization B bypass). The vulnerability was fixed in FreeScout 1.8.212, as noted in CVE-2026-35584 and associated disclosures (CWE mappings included). The flaws demonstrate how missing authentication and improper ownership checks in Laravel controllers can leak or mutate state when route parameters are not properly constrained. In real-world Laravel deployments, similar patterns can arise if routes rely solely on scalar IDs without binding and authorization checks.\n\nExploitation typically starts with an attacker sending crafted requests to the endpoint with arbitrary conversation_id and thread_id values. The app would respond with 200 or 404 depending on whether the combination exists, enabling enumeration of valid thread IDs. Once a valid thread is found, the attacker could manipulate fields like opened_at to reflect access across unrelated conversations, thereby corrupting audit trails or user state. This demonstrates the necessity of explicit authentication, proper authorization, and ownership validation in Laravel’s request handling, particularly for endpoints that can affect stored state or user-visible indicators.\n\nTo prevent these issues in Laravel, enforce authentication on sensitive endpoints, validate input, and verify that the authenticated user has legitimate access to the targeted resources. Use route model binding with authorization policies, ensure the thread belongs to the conversation in question, and avoid performing state-changing actions in endpoints that are not protected by authentication and authorization. Upgrading to the fixed version (FreeScout 1.8.212) and adopting policy-based access control are essential to close the gap.\n\nIn addition to the specific patch, consider adopting Laravel-specific hardening: apply auth middleware at the route level, implement a robust ThreadPolicy with read/update controls, prefer explicit ownership checks, and cover these paths with automated tests that simulate unauthorized access attempts and cross-resource access attempts.

Affected Versions

1.8.0 - 1.8.211 (FreeScout) prior to 1.8.212

Code Fix Example

Laravel API Security Remediation
VULNERABLE:
Route::get('/thread/read/{conversation}/{thread}', [ThreadController::class, 'readThread']);

public function readThread($conversationId, $threadId) {
    $thread = Thread::where('conversation_id', $conversationId)
                    ->where('id', $threadId)
                    ->first();
    if (!$thread) {
        return response()->json(['error' => 'Not found'], 404);
    }
    // Mark as read without auth/ownership checks
    $thread->update(['opened_at' => now()]);
    return response()->json($thread);
}

FIX:
Route::get('/thread/read/{conversation}/{thread}', [ThreadController::class, 'readThread'])->middleware('auth');

public function readThread(Request $request, Conversation $conversation, Thread $thread) {
    // Ensure the thread actually belongs to the provided conversation
    if ($conversation->id !== $thread->conversation_id) {
        return response()->json(['error' => 'Not found'], 404);
    }
    // Authorization check: ensure the authenticated user has access to this thread
    if ($request->user()->cannot('read', $thread)) {
        return response()->json(['error' => 'Forbidden'], 403);
    }
    $thread->update(['opened_at' => now()]);
    return response()->json($thread);
}

CVE References

Choose which optional cookies to allow. You can change this any time.