diff --git a/routes/api.php b/routes/api.php index c6f5a67f..fe3c48f1 100644 --- a/routes/api.php +++ b/routes/api.php @@ -13,7 +13,7 @@ use Illuminate\Support\Facades\Route; Route::post('/uploads/{token}', [UploadController::class, 'store']) - ->middleware('signed') + ->middleware(['signed', 'throttle:10,1']) ->where('token', '[0-9a-f-]{36}') ->name('api.uploads.store'); diff --git a/tests/Feature/Api/UploadControllerTest.php b/tests/Feature/Api/UploadControllerTest.php index 2d79584a..ac69900e 100644 --- a/tests/Feature/Api/UploadControllerTest.php +++ b/tests/Feature/Api/UploadControllerTest.php @@ -121,3 +121,19 @@ function signedUploadUrl(Workspace $ws, string $token, ?int $expiresInMinutes = $response->assertStatus(422); expect(Media::where('upload_token', $token)->exists())->toBeFalse(); }); + +test('rate limits floods from the same IP', function () { + for ($i = 0; $i < 10; $i++) { + $this->postJson( + signedUploadUrl($this->workspace, (string) Str::uuid()), + ['media' => UploadedFile::fake()->image("f{$i}.png", 16, 16)], + ); + } + + $response = $this->postJson( + signedUploadUrl($this->workspace, (string) Str::uuid()), + ['media' => UploadedFile::fake()->image('over.png', 16, 16)], + ); + + $response->assertStatus(429); +});