|
|
@@ -89,4 +89,56 @@ final class RateLimitTest extends AppTestCase
|
|
|
self::assertNotSame(429, $resp->getStatusCode());
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * SEC_REVIEW F14. The /api/v1/auth/* group previously had only
|
|
|
+ * TokenAuth, so a service-token holder could brute-force enumerate
|
|
|
+ * users via /users/{id} (F17) at unbounded rate, or burn unbounded
|
|
|
+ * upsert calls. RateLimitMiddleware is now attached; a burst against
|
|
|
+ * the auth endpoints must produce 429s once the bucket drains.
|
|
|
+ */
|
|
|
+ public function testAuthGetUserRouteIsRateLimited(): void
|
|
|
+ {
|
|
|
+ $service = $this->createToken(TokenKind::Service);
|
|
|
+ $headers = ['Authorization' => 'Bearer ' . $service];
|
|
|
+
|
|
|
+ $statuses = [];
|
|
|
+ for ($i = 0; $i < 20; $i++) {
|
|
|
+ $statuses[] = $this->request('GET', '/api/v1/auth/users/1', $headers)->getStatusCode();
|
|
|
+ }
|
|
|
+
|
|
|
+ $limited = count(array_filter($statuses, static fn (int $s): bool => $s === 429));
|
|
|
+ self::assertGreaterThan(
|
|
|
+ 0,
|
|
|
+ $limited,
|
|
|
+ 'auth/users/{id} must hit the rate-limit ceiling on burst (SEC_REVIEW F14)'
|
|
|
+ );
|
|
|
+ // Capacity is 4 (refill=2, capacity=4 from setUp); the rest must 429.
|
|
|
+ self::assertSame(16, $limited, 'remainder rate-limited');
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * SEC_REVIEW F14. upsertLocal/upsertOidc are also gated by the same
|
|
|
+ * rate limit — a leaked service token cannot pound them at unbounded
|
|
|
+ * rate to amplify other findings (e.g. burning audit rows, retrying
|
|
|
+ * upsert combinations).
|
|
|
+ */
|
|
|
+ public function testAuthUpsertLocalRouteIsRateLimited(): void
|
|
|
+ {
|
|
|
+ $service = $this->createToken(TokenKind::Service);
|
|
|
+ $headers = [
|
|
|
+ 'Authorization' => 'Bearer ' . $service,
|
|
|
+ 'Content-Type' => 'application/json',
|
|
|
+ ];
|
|
|
+ $body = json_encode(['username' => 'admin']) ?: null;
|
|
|
+
|
|
|
+ $statuses = [];
|
|
|
+ for ($i = 0; $i < 20; $i++) {
|
|
|
+ $statuses[] = $this->request('POST', '/api/v1/auth/users/upsert-local', $headers, $body)
|
|
|
+ ->getStatusCode();
|
|
|
+ }
|
|
|
+
|
|
|
+ $limited = count(array_filter($statuses, static fn (int $s): bool => $s === 429));
|
|
|
+ self::assertGreaterThan(0, $limited, 'upsert-local must rate-limit');
|
|
|
+ }
|
|
|
}
|