From dd5bcdd3512f3c90da8747c1ee8beb98961508df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Munsch?= Date: Wed, 26 Aug 2026 09:41:40 +0200 Subject: [PATCH] fix(security): sanitize rate limiter client IP resolution to prevent header spoofing (F-2026-0001) --- src/Infra/Providers/AppServiceProvider.php | 12 +++---- tests/Feature/Api/V1/RateLimitingTest.php | 41 ++++++++++++++++++++++ 2 files changed, 45 insertions(+), 8 deletions(-) create mode 100644 tests/Feature/Api/V1/RateLimitingTest.php diff --git a/src/Infra/Providers/AppServiceProvider.php b/src/Infra/Providers/AppServiceProvider.php index 9541c59..f9e75e9 100644 --- a/src/Infra/Providers/AppServiceProvider.php +++ b/src/Infra/Providers/AppServiceProvider.php @@ -25,10 +25,6 @@ class AppServiceProvider extends ServiceProvider #[\Override] public function register(): void { - $this->app->singleton('clientIp', function () { - return request()->server('HTTP_X_FORWARDED_FOR') ?? request()->getClientIp(); - }); - $this->app->bind( HealthChecker::class, LaravelSystemHealthChecker::class @@ -81,12 +77,12 @@ class AppServiceProvider extends ServiceProvider protected function configureRateLimiting(ThrottlingConfig $throttlingConfig): void { - RateLimiter::for('web', function (IlluminateRequest $_) use ($throttlingConfig) { - return Limit::perMinute($throttlingConfig->webLimit->toInt())->by(resolve('clientIp')); + RateLimiter::for('web', function (IlluminateRequest $request) use ($throttlingConfig) { + return Limit::perMinute($throttlingConfig->webLimit->toInt())->by($request->ip() ?? '127.0.0.1'); }); - RateLimiter::for('api.v1', function (IlluminateRequest $_) use ($throttlingConfig) { - return Limit::perMinute($throttlingConfig->apiV1Limit->toInt())->by(resolve('clientIp')); + RateLimiter::for('api.v1', function (IlluminateRequest $request) use ($throttlingConfig) { + return Limit::perMinute($throttlingConfig->apiV1Limit->toInt())->by($request->ip() ?? '127.0.0.1'); }); } } diff --git a/tests/Feature/Api/V1/RateLimitingTest.php b/tests/Feature/Api/V1/RateLimitingTest.php new file mode 100644 index 0000000..cafb65c --- /dev/null +++ b/tests/Feature/Api/V1/RateLimitingTest.php @@ -0,0 +1,41 @@ +withServerVariables(['REMOTE_ADDR' => '192.168.1.100']) + ->getJson('/api/v1/health'); + + // THEN + $response->assertStatus(200); + expect($response->headers->has('X-RateLimit-Limit'))->toBeTrue(); + expect($response->headers->has('X-RateLimit-Remaining'))->toBeTrue(); + }); + + it('does not resolve rate limit key from untrusted raw X-Forwarded-For header', function (): void { + // GIVEN + /** @var \Tests\TestCase $this */ + $clientIp = '10.20.30.40'; + + // WHEN: Making request with spoofed X-Forwarded-For header + $response = $this->withServerVariables(['REMOTE_ADDR' => $clientIp]) + ->withHeaders(['X-Forwarded-For' => '203.0.113.195']) + ->getJson('/api/v1/health'); + + // THEN + $response->assertStatus(200); + }); +});