From 5a7c6ac6e5af1622a20a4230e3876a086708530a Mon Sep 17 00:00:00 2001 From: Boy132 Date: Wed, 9 Jul 2025 13:51:43 +0200 Subject: [PATCH] Improve turnstile error handling (+ cleanup) (#1501) --- app/Extensions/Captcha/Schemas/BaseSchema.php | 5 --- .../Schemas/CaptchaSchemaInterface.php | 7 +--- .../Captcha/Schemas/Turnstile/Rule.php | 9 +++-- .../Schemas/Turnstile/TurnstileSchema.php | 31 ++++++++++----- app/Http/Middleware/VerifyCaptcha.php | 39 ------------------- bootstrap/app.php | 1 - 6 files changed, 28 insertions(+), 64 deletions(-) delete mode 100644 app/Http/Middleware/VerifyCaptcha.php diff --git a/app/Extensions/Captcha/Schemas/BaseSchema.php b/app/Extensions/Captcha/Schemas/BaseSchema.php index 8d7d028ea..fda3bbbb0 100644 --- a/app/Extensions/Captcha/Schemas/BaseSchema.php +++ b/app/Extensions/Captcha/Schemas/BaseSchema.php @@ -56,9 +56,4 @@ abstract class BaseSchema ->default(env("CAPTCHA_{$id}_SECRET_KEY")), ]; } - - public function verifyDomain(string $hostname, ?string $requestUrl = null): bool - { - return true; - } } diff --git a/app/Extensions/Captcha/Schemas/CaptchaSchemaInterface.php b/app/Extensions/Captcha/Schemas/CaptchaSchemaInterface.php index 908ecaa08..1b02b7b6e 100644 --- a/app/Extensions/Captcha/Schemas/CaptchaSchemaInterface.php +++ b/app/Extensions/Captcha/Schemas/CaptchaSchemaInterface.php @@ -26,10 +26,5 @@ interface CaptchaSchemaInterface public function getIcon(): ?string; - /** - * @return array - */ - public function validateResponse(?string $captchaResponse = null): array; - - public function verifyDomain(string $hostname, ?string $requestUrl = null): bool; + public function validateResponse(?string $captchaResponse = null): void; } diff --git a/app/Extensions/Captcha/Schemas/Turnstile/Rule.php b/app/Extensions/Captcha/Schemas/Turnstile/Rule.php index 05775fd59..2f0af2622 100644 --- a/app/Extensions/Captcha/Schemas/Turnstile/Rule.php +++ b/app/Extensions/Captcha/Schemas/Turnstile/Rule.php @@ -4,6 +4,7 @@ namespace App\Extensions\Captcha\Schemas\Turnstile; use App\Extensions\Captcha\CaptchaService; use Closure; +use Exception; use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Support\Facades\App; @@ -11,10 +12,12 @@ class Rule implements ValidationRule { public function validate(string $attribute, mixed $value, Closure $fail): void { - $response = App::call(fn (CaptchaService $service) => $service->getActiveSchema()->validateResponse($value)); + try { + App::call(fn (CaptchaService $service) => $service->get('turnstile')->validateResponse($value)); + } catch (Exception $exception) { + report($exception); - if (!$response['success']) { - $fail($response['message'] ?? 'Unknown error occurred, please try again'); + $fail('Captcha validation failed: ' . $exception->getMessage()); } } } diff --git a/app/Extensions/Captcha/Schemas/Turnstile/TurnstileSchema.php b/app/Extensions/Captcha/Schemas/Turnstile/TurnstileSchema.php index 07b0280e9..299d4914c 100644 --- a/app/Extensions/Captcha/Schemas/Turnstile/TurnstileSchema.php +++ b/app/Extensions/Captcha/Schemas/Turnstile/TurnstileSchema.php @@ -66,9 +66,9 @@ class TurnstileSchema extends BaseSchema implements CaptchaSchemaInterface } /** - * @return array + * @throws Exception */ - public function validateResponse(?string $captchaResponse = null): array + public function validateResponse(?string $captchaResponse = null): void { $captchaResponse ??= request()->get('cf-turnstile-response'); @@ -83,22 +83,33 @@ class TurnstileSchema extends BaseSchema implements CaptchaSchemaInterface ->post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [ 'secret' => $secret, 'response' => $captchaResponse, - ]); + ]) + ->json(); - return count($response->json()) ? $response->json() : [ - 'success' => false, - 'message' => 'Unknown error occurred, please try again', - ]; + if (!$response['success']) { + match ($response['error-codes'][0] ?? null) { + 'missing-input-secret' => throw new Exception('The secret parameter was not passed.'), + 'invalid-input-secret' => throw new Exception('The secret parameter was invalid, did not exist, or is a testing secret key with a non-testing response.'), + 'missing-input-response' => throw new Exception('The response parameter (token) was not passed.'), + 'invalid-input-response' => throw new Exception('The response parameter (token) is invalid or has expired.'), + 'bad-request' => throw new Exception('The request was rejected because it was malformed.'), + 'timeout-or-duplicate' => throw new Exception('The response parameter (token) has already been validated before.'), + default => throw new Exception('An internal error happened while validating the response.'), + }; + } + + if (!$this->verifyDomain($response['hostname'] ?? '')) { + throw new Exception('Domain verification failed.'); + } } - public function verifyDomain(string $hostname, ?string $requestUrl = null): bool + private function verifyDomain(string $hostname): bool { if (!env('CAPTCHA_TURNSTILE_VERIFY_DOMAIN', true)) { return true; } - $requestUrl ??= request()->url; - $requestUrl = parse_url($requestUrl); + $requestUrl = parse_url(request()->url()); return $hostname === array_get($requestUrl, 'host'); } diff --git a/app/Http/Middleware/VerifyCaptcha.php b/app/Http/Middleware/VerifyCaptcha.php deleted file mode 100644 index 4e06b10fb..000000000 --- a/app/Http/Middleware/VerifyCaptcha.php +++ /dev/null @@ -1,39 +0,0 @@ -app->isLocal()) { - return $next($request); - } - - $schemas = $captchaService->getActiveSchemas(); - foreach ($schemas as $schema) { - $response = $schema->validateResponse(); - - if ($response['success'] && $schema->verifyDomain($response['hostname'] ?? '', $request->url())) { - return $next($request); - } - - event(new FailedCaptcha($request->ip(), $response['message'] ?? null)); - - throw new HttpException(Response::HTTP_BAD_REQUEST, "Failed to validate {$schema->getId()} captcha data."); - } - - // No captcha enabled - return $next($request); - } -} diff --git a/bootstrap/app.php b/bootstrap/app.php index 8d8ed0f97..3892fa74f 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -44,7 +44,6 @@ return Application::configure(basePath: dirname(__DIR__)) 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'node.maintenance' => \App\Http\Middleware\MaintenanceMiddleware::class, - 'captcha' => \App\Http\Middleware\VerifyCaptcha::class, ]); }) ->withSingletons([