VYPR
High severity7.5NVD Advisory· Published Jul 9, 2024· Updated Apr 15, 2026

CVE-2024-36676

CVE-2024-36676

Description

Incorrect access control in BookStack before v24.05.1 allows attackers to confirm existing system users and perform targeted notification email DoS via public facing forms.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
ssddanbrown/bookstackPackagist
< 24.05.124.05.1

Patches

2
69af9e0dbdef

Routes: Added throttling to a range of auth-related endpoints

https://github.com/BookStackApp/BookStackDan BrownMay 20, 2024via ghsa
7 files changed · +109 12
  • app/Access/Controllers/ForgotPasswordController.php+5 0 modified
    @@ -6,6 +6,7 @@
     use BookStack\Http\Controller;
     use Illuminate\Http\Request;
     use Illuminate\Support\Facades\Password;
    +use Illuminate\Support\Sleep;
     
     class ForgotPasswordController extends Controller
     {
    @@ -32,6 +33,10 @@ public function sendResetLinkEmail(Request $request)
                 'email' => ['required', 'email'],
             ]);
     
    +        // Add random pause to the response to help avoid time-base sniffing
    +        // of valid resets via slower email send handling.
    +        Sleep::for(random_int(1000, 3000))->milliseconds();
    +
             // We will send the password reset link to this user. Once we have attempted
             // to send the link, we will examine the response then see the message we
             // need to show to the user. Finally, we'll send out a proper response.
    
  • app/Access/Controllers/ResetPasswordController.php+3 6 modified
    @@ -15,14 +15,11 @@
     
     class ResetPasswordController extends Controller
     {
    -    protected LoginService $loginService;
    -
    -    public function __construct(LoginService $loginService)
    -    {
    +    public function __construct(
    +        protected LoginService $loginService
    +    ) {
             $this->middleware('guest');
             $this->middleware('guard:standard');
    -
    -        $this->loginService = $loginService;
         }
     
         /**
    
  • app/App/Providers/RouteServiceProvider.php+4 0 modified
    @@ -81,5 +81,9 @@ protected function configureRateLimiting(): void
             RateLimiter::for('api', function (Request $request) {
                 return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
             });
    +
    +        RateLimiter::for('public', function (Request $request) {
    +            return Limit::perMinute(10)->by($request->ip());
    +        });
         }
     }
    
  • routes/web.php+6 6 modified
    @@ -317,8 +317,8 @@
     Route::get('/register/confirm/awaiting', [AccessControllers\ConfirmEmailController::class, 'showAwaiting']);
     Route::post('/register/confirm/resend', [AccessControllers\ConfirmEmailController::class, 'resend']);
     Route::get('/register/confirm/{token}', [AccessControllers\ConfirmEmailController::class, 'showAcceptForm']);
    -Route::post('/register/confirm/accept', [AccessControllers\ConfirmEmailController::class, 'confirm']);
    -Route::post('/register', [AccessControllers\RegisterController::class, 'postRegister']);
    +Route::post('/register/confirm/accept', [AccessControllers\ConfirmEmailController::class, 'confirm'])->middleware('throttle:public');
    +Route::post('/register', [AccessControllers\RegisterController::class, 'postRegister'])->middleware('throttle:public');
     
     // SAML routes
     Route::post('/saml2/login', [AccessControllers\Saml2Controller::class, 'login']);
    @@ -338,16 +338,16 @@
     Route::post('/oidc/logout', [AccessControllers\OidcController::class, 'logout']);
     
     // User invitation routes
    -Route::get('/register/invite/{token}', [AccessControllers\UserInviteController::class, 'showSetPassword']);
    -Route::post('/register/invite/{token}', [AccessControllers\UserInviteController::class, 'setPassword']);
    +Route::get('/register/invite/{token}', [AccessControllers\UserInviteController::class, 'showSetPassword'])->middleware('throttle:public');
    +Route::post('/register/invite/{token}', [AccessControllers\UserInviteController::class, 'setPassword'])->middleware('throttle:public');
     
     // Password reset link request routes
     Route::get('/password/email', [AccessControllers\ForgotPasswordController::class, 'showLinkRequestForm']);
    -Route::post('/password/email', [AccessControllers\ForgotPasswordController::class, 'sendResetLinkEmail']);
    +Route::post('/password/email', [AccessControllers\ForgotPasswordController::class, 'sendResetLinkEmail'])->middleware('throttle:public');
     
     // Password reset routes
     Route::get('/password/reset/{token}', [AccessControllers\ResetPasswordController::class, 'showResetForm']);
    -Route::post('/password/reset', [AccessControllers\ResetPasswordController::class, 'reset']);
    +Route::post('/password/reset', [AccessControllers\ResetPasswordController::class, 'reset'])->middleware('throttle:public');
     
     // Metadata routes
     Route::view('/help/wysiwyg', 'help.wysiwyg');
    
  • tests/Auth/RegistrationTest.php+29 0 modified
    @@ -203,4 +203,33 @@ public function test_registration_simple_honeypot_active()
             $resp = $this->followRedirects($resp);
             $this->withHtml($resp)->assertElementExists('form input[name="username"].text-neg');
         }
    +
    +    public function test_registration_endpoint_throttled()
    +    {
    +        $this->setSettings(['registration-enabled' => 'true']);
    +
    +        for ($i = 0; $i < 11; $i++) {
    +            $response = $this->post('/register/', [
    +                'name' => "Barry{$i}",
    +                'email' => "barry{$i}@example.com",
    +                'password' => "barryIsTheBest{$i}",
    +            ]);
    +            auth()->logout();
    +        }
    +
    +        $response->assertStatus(429);
    +    }
    +
    +    public function test_registration_confirmation_throttled()
    +    {
    +        $this->setSettings(['registration-enabled' => 'true']);
    +
    +        for ($i = 0; $i < 11; $i++) {
    +            $response = $this->post('/register/confirm/accept', [
    +                'token' => "token{$i}",
    +            ]);
    +        }
    +
    +        $response->assertStatus(429);
    +    }
     }
    
  • tests/Auth/ResetPasswordTest.php+42 0 modified
    @@ -4,11 +4,19 @@
     
     use BookStack\Access\Notifications\ResetPasswordNotification;
     use BookStack\Users\Models\User;
    +use Carbon\CarbonInterval;
     use Illuminate\Support\Facades\Notification;
    +use Illuminate\Support\Sleep;
     use Tests\TestCase;
     
     class ResetPasswordTest extends TestCase
     {
    +    protected function setUp(): void
    +    {
    +        parent::setUp();
    +        Sleep::fake();
    +    }
    +
         public function test_reset_flow()
         {
             Notification::fake();
    @@ -75,6 +83,17 @@ public function test_reset_flow_shows_success_message_even_if_wrong_password_to_
                 ->assertSee('The password reset token is invalid for this email address.');
         }
     
    +    public function test_reset_request_with_not_found_user_still_has_delay()
    +    {
    +        $this->followingRedirects()->post('/password/email', [
    +            'email' => 'barrynotfoundrandomuser@example.com',
    +        ]);
    +
    +        Sleep::assertSlept(function (CarbonInterval $duration): bool {
    +            return $duration->totalMilliseconds > 999;
    +        }, 1);
    +    }
    +
         public function test_reset_page_shows_sign_links()
         {
             $this->setSettings(['registration-enabled' => 'true']);
    @@ -98,4 +117,27 @@ public function test_reset_request_is_throttled()
             Notification::assertSentTimes(ResetPasswordNotification::class, 1);
             $resp->assertSee('A password reset link will be sent to ' . $editor->email . ' if that email address is found in the system.');
         }
    +
    +    public function test_reset_request_with_not_found_user_is_throttled()
    +    {
    +        for ($i = 0; $i < 11; $i++) {
    +            $response = $this->post('/password/email', [
    +                'email' => 'barrynotfoundrandomuser@example.com',
    +            ]);
    +        }
    +
    +        $response->assertStatus(429);
    +    }
    +
    +    public function test_reset_call_is_throttled()
    +    {
    +        for ($i = 0; $i < 11; $i++) {
    +            $response = $this->post('/password/reset', [
    +                'email' => "arandomuser{$i}@example.com",
    +                'token' => "randomtoken{$i}",
    +            ]);
    +        }
    +
    +        $response->assertStatus(429);
    +    }
     }
    
  • tests/Auth/UserInviteTest.php+20 0 modified
    @@ -137,4 +137,24 @@ public function test_token_expires_after_two_weeks()
             $setPasswordPageResp->assertRedirect('/password/email');
             $setPasswordPageResp->assertSessionHas('error', 'This invitation link has expired. You can instead try to reset your account password.');
         }
    +
    +    public function test_set_password_view_is_throttled()
    +    {
    +        for ($i = 0; $i < 11; $i++) {
    +            $response = $this->get("/register/invite/tokenhere{$i}");
    +        }
    +
    +        $response->assertStatus(429);
    +    }
    +
    +    public function test_set_password_post_is_throttled()
    +    {
    +        for ($i = 0; $i < 11; $i++) {
    +            $response = $this->post("/register/invite/tokenhere{$i}", [
    +                'password' => 'my test password',
    +            ]);
    +        }
    +
    +        $response->assertStatus(429);
    +    }
     }
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.