From 77800174c99da752d2e9339ca93e88d4b69e679d Mon Sep 17 00:00:00 2001 From: Timothy Allen Date: Sat, 26 Sep 2026 12:42:19 +0200 Subject: [PATCH] Use the reCAPTCHA Enterprise API. --- .env.example | 9 +- README.md | 15 +++ captcha.lib.php | 81 ++++++++++----- publish/login.php | 4 +- tests/php/Pages/LoginTest.php | 15 ++- tests/php/Pages/SubmitTest.php | 19 +++- tests/php/Support/FakeRecaptcha.php | 7 +- tests/php/Unit/CaptchaTest.php | 150 ++++++++++++++++++++++++---- tests/php/Unit/ValidateDataTest.php | 8 +- tests/php/fixtures/router.php | 14 ++- tests/php/fixtures/test.env | 3 +- 11 files changed, 265 insertions(+), 60 deletions(-) diff --git a/.env.example b/.env.example index e9cf63f..ef1d6ac 100644 --- a/.env.example +++ b/.env.example @@ -13,10 +13,15 @@ DB_NAME=xxx # host. If it is empty, the pages refuse plain HTTP. SITE_URL=https://xxx.org.za -# Google reCAPTCHA v2 keys (https://www.google.com/recaptcha/admin). +# Google reCAPTCHA Enterprise checkbox key and its Google Cloud project ID. +# Enable the reCAPTCHA Enterprise API in that project. RECAPTCHA_API_KEY is +# a server-side Google Cloud API key, not the old reCAPTCHA secret. Restrict +# it to that API and, if possible, the server's outbound IP address. +# SITE_URL must have the same hostname as the page that shows the checkbox. # Leave RECAPTCHA_SITE_KEY empty to turn the captcha off. RECAPTCHA_SITE_KEY= -RECAPTCHA_SECRET= +RECAPTCHA_PROJECT_ID= +RECAPTCHA_API_KEY= # Google Analytics tracking ID. Leave it empty to turn analytics off. GOOGLE_ANALYTICS_ID= diff --git a/README.md b/README.md index 58058de..53862a5 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,21 @@ fancy new PHP features... that's probably why. The site reads its configuration from `.env`. Copy `.env.example` to `.env`, and set the values in it. +## reCAPTCHA + +To enable reCAPTCHA, set up the Google Cloud credentials: + +1. Enable the reCAPTCHA Enterprise API in the project that owns your + checkbox key. +2. Set `RECAPTCHA_SITE_KEY` to that checkbox key in `.env`. +3. Set `RECAPTCHA_PROJECT_ID` to the Google Cloud project ID. +4. Create a Google Cloud API key for server requests. +5. Restrict the API key to the reCAPTCHA Enterprise API. +6. If the server has a fixed outbound IP address, restrict the API key + to that address too. +7. Set `RECAPTCHA_API_KEY` to the API key in `.env`. +8. Set `SITE_URL` to the site's HTTPS address. + ## Tests The repository has three sets of tests: diff --git a/captcha.lib.php b/captcha.lib.php index 4c5ec1a..2106ef5 100644 --- a/captcha.lib.php +++ b/captcha.lib.php @@ -1,12 +1,11 @@ '; + return ''; } -function captcha_widget() { +function captcha_widget($action = 'submit') { if (!captcha_enabled()) return ''; - return '
'; + return '
'; } -// Ask Google whether the captcha in this POST was solved -function captcha_verify_response() { +// The expected action comes from the page, never from the request +function captcha_verify_response($action = 'submit') { if (!captcha_enabled()) return TRUE; $response = $_POST['g-recaptcha-response'] ?? ''; if (!is_string($response) || $response === '') return FALSE; - $secret = (string)config('RECAPTCHA_SECRET'); - if ($secret === '') { - error_log('xxx: RECAPTCHA_SECRET is not set, so every captcha check fails'); + $project = trim((string)config('RECAPTCHA_PROJECT_ID')); + $api_key = trim((string)config('RECAPTCHA_API_KEY')); + $host = strtolower(site_host()); + if ($project === '' || $api_key === '' || $host === '') { + error_log('xxx: reCAPTCHA needs RECAPTCHA_PROJECT_ID, RECAPTCHA_API_KEY and a SITE_URL with a host'); return FALSE; } - $context = stream_context_create(array('http' => array( - 'method' => 'POST', - 'header' => "Content-Type: application/x-www-form-urlencoded\r\n", - 'content' => http_build_query(array( - 'secret' => $secret, - 'response' => $response, - 'remoteip' => $_SERVER['REMOTE_ADDR'] ?? '', - )), - 'timeout' => 10, + if (preg_match('/[\r\n]/', $api_key)) { + error_log('xxx: RECAPTCHA_API_KEY contains a line break'); + return FALSE; + } + $body = json_encode(array('event' => array( + 'token' => $response, + 'siteKey' => (string)config('RECAPTCHA_SITE_KEY'), + 'expectedAction' => $action, + 'userIpAddress' => $_SERVER['REMOTE_ADDR'] ?? '', ))); - $reply = @file_get_contents('https://www.google.com/recaptcha/api/siteverify', FALSE, $context); + if ($body === FALSE) + return FALSE; + // Keep the API key out of URLs and logs, and do not forward it on redirects + $context = stream_context_create(array('http' => array( + 'method' => 'POST', + 'header' => "Content-Type: application/json\r\nX-Goog-Api-Key: $api_key\r\n", + 'content' => $body, + 'timeout' => 10, + 'follow_location' => 0, + 'ignore_errors' => FALSE, + ))); + $url = 'https://recaptchaenterprise.googleapis.com/v1/projects/'.rawurlencode($project).'/assessments'; + $reply = @file_get_contents($url, FALSE, $context); if ($reply === FALSE) { - error_log('xxx: could not reach the reCAPTCHA server'); + error_log('xxx: reCAPTCHA assessment request failed'); return FALSE; } $result = json_decode($reply); - return is_object($result) && !empty($result->success); + if (!is_object($result) || isset($result->error) || !is_object($result->tokenProperties ?? NULL)) { + error_log('xxx: reCAPTCHA returned an invalid assessment or an API error'); + return FALSE; + } + $token = $result->tokenProperties; + if (($token->valid ?? FALSE) !== TRUE) + return FALSE; + if (!is_string($token->hostname ?? NULL) || strtolower($token->hostname) !== $host) { + error_log('xxx: reCAPTCHA hostname did not match SITE_URL'); + return FALSE; + } + if (($token->action ?? NULL) !== $action) { + error_log('xxx: reCAPTCHA action did not match the page'); + return FALSE; + } + // Keep checkbox behavior; do not reject visitors by a risk score + return TRUE; } /* diff --git a/publish/login.php b/publish/login.php index 529e4df..9c1253a 100644 --- a/publish/login.php +++ b/publish/login.php @@ -16,7 +16,7 @@ if (isset($_SESSION['editor'])) { } else if (isset($_POST['login']) && ($wait = login_blocked_for($login_name)) > 0) { $denied = TRUE; $wait_minutes = (int)ceil($wait / 60); -} else if (isset($_POST['login']) && !captcha_verify_response()) { +} else if (isset($_POST['login']) && !captcha_verify_response('login')) { $denied = TRUE; $captcha_failed = TRUE; } else if (isset($_POST['login'])) { @@ -113,7 +113,7 @@ else if ($denied) - +
diff --git a/tests/php/Pages/LoginTest.php b/tests/php/Pages/LoginTest.php index d955685..6e91b29 100644 --- a/tests/php/Pages/LoginTest.php +++ b/tests/php/Pages/LoginTest.php @@ -50,7 +50,8 @@ class LoginTest extends PageTestCase { public function testTheFormHasATokenAndTheCaptcha(): void { $body = $this->get('/publish/login')['body']; $this->assertSame($this->session()['csrf_token'], $this->csrfToken($body)); - $this->assertStringContainsString('data-sitekey="test-site-key"', $body); + $this->assertStringContainsString('data-sitekey="test-site-key" data-action="login"', $body); + $this->assertStringContainsString('https://www.google.com/recaptcha/enterprise.js', $body); } public function testTheRightPasswordLogsIn(): void { @@ -130,6 +131,18 @@ class LoginTest extends PageTestCase { $this->assertSame(array(), $this->queries('/FROM editors/')); } + public static function failedAssessments(): array { + return array(array('wrong-host'), array('wrong-action'), array('api-error'), array('unreachable')); + } + + #[DataProvider('failedAssessments')] + public function testAFailedAssessmentBlocksTheLogin(string $response): void { + $answer = $this->login('secret', array('g-recaptcha-response' => $response)); + $this->assertStringContainsString('Please complete the captcha.', $answer['body']); + $this->assertSame(array(), $this->queries('/FROM editors/')); + $this->assertArrayNotHasKey('editor', $this->session()); + } + public function testFiveFailuresBlockTheLogin(): void { for ($i = 0; $i < 5; $i++) $this->login('wrong'); diff --git a/tests/php/Pages/SubmitTest.php b/tests/php/Pages/SubmitTest.php index cf5dab4..139c9b1 100644 --- a/tests/php/Pages/SubmitTest.php +++ b/tests/php/Pages/SubmitTest.php @@ -1,6 +1,7 @@ get('/publish'); $this->assertSame(200, $page['status']); $this->assertSame($this->session()['csrf_token'], $this->csrfToken($page['body'])); - $this->assertStringContainsString('
', $page['body']); - $this->assertStringContainsString('https://www.google.com/recaptcha/api.js', $page['body']); + $this->assertStringContainsString('
', $page['body']); + $this->assertStringContainsString('https://www.google.com/recaptcha/enterprise.js', $page['body']); $this->assertStringContainsString('', $page['body']); } @@ -104,6 +105,20 @@ class SubmitTest extends PageTestCase { $this->assertSame(array(), $this->queries('/^INSERT/')); } + public static function failedAssessments(): array { + return array(array('wrong-host'), array('wrong-action'), array('api-error'), array('unreachable')); + } + + #[DataProvider('failedAssessments')] + public function testAFailedAssessmentBlocksTheSubmission(string $response): void { + $token = $this->csrfToken($this->get('/publish')['body']); + $page = $this->post('/publish', $this->story(array('action' => 'submit', 'csrf_token' => $token, 'g-recaptcha-response' => $response))); + $this->assertStringContainsString('Please complete the captcha.', $page['body']); + $this->assertSame(array(), $this->queries('/^INSERT/')); + $this->assertSame(array(), $this->mails()); + $this->assertArrayNotHasKey('captcha_ok', $this->session()); + } + public function testMissingFieldsAreShown(): void { $token = $this->csrfToken($this->get('/publish')['body']); $page = $this->post('/publish', array('action' => 'submit', 'csrf_token' => $token, 'g-recaptcha-response' => 'pass', 'story' => '

x

')); diff --git a/tests/php/Support/FakeRecaptcha.php b/tests/php/Support/FakeRecaptcha.php index fb8a2ee..ba0f368 100644 --- a/tests/php/Support/FakeRecaptcha.php +++ b/tests/php/Support/FakeRecaptcha.php @@ -10,15 +10,16 @@ namespace Xxx\Tests\Support; * returns one of these. */ class FakeRecaptcha { + public const PASS = '{"tokenProperties":{"valid":true,"hostname":"site.test","action":"submit"}}'; public static array $requests = array(); - public static $reply = '{"success": true}'; + public static $reply = self::PASS; private static bool $active = FALSE; public $context; private string $data = ''; private int $position = 0; - public static function install($reply = '{"success": true}'): void { + public static function install($reply = self::PASS): void { self::$requests = array(); self::$reply = $reply; if (!self::$active) { @@ -38,7 +39,7 @@ class FakeRecaptcha { public function stream_open($path, $mode, $options, &$opened_path) { $context = $this->context ? stream_context_get_options($this->context) : array(); $http = $context['http'] ?? array(); - parse_str((string)($http['content'] ?? ''), $post); + $post = json_decode((string)($http['content'] ?? ''), TRUE) ?? array(); self::$requests[] = array('url' => $path, 'http' => $http, 'post' => $post); $reply = self::$reply instanceof \Closure ? (self::$reply)($post) : self::$reply; if ($reply === FALSE) diff --git a/tests/php/Unit/CaptchaTest.php b/tests/php/Unit/CaptchaTest.php index c4fc38e..d2a50dd 100644 --- a/tests/php/Unit/CaptchaTest.php +++ b/tests/php/Unit/CaptchaTest.php @@ -1,6 +1,7 @@ assertSame('', captcha_script()); - $this->assertSame('
', captcha_widget()); + $this->assertSame('', captcha_script()); + $this->assertSame('
', captcha_widget()); + $this->assertSame('
', captcha_widget('login')); } public function testWidgetEscapesTheSiteKey(): void { $this->setEnv('RECAPTCHA_SITE_KEY', '">'); - $this->assertSame('
', captcha_widget()); + $this->assertSame('
', captcha_widget()); + } + + public function testWidgetEscapesTheAction(): void { + $this->assertSame('
', + captcha_widget('">')); } public function testNothingFromGoogleWithoutASiteKey(): void { @@ -51,55 +58,164 @@ class CaptchaTest extends TestCase { } public function testSolvedCaptchaPasses(): void { - FakeRecaptcha::install('{"success": true, "hostname": "xxx.org.za"}'); + FakeRecaptcha::install(); $_SERVER['REMOTE_ADDR'] = '198.51.100.7'; $this->request(array(), array('g-recaptcha-response' => 'token-from-widget')); $this->assertTrue(captcha_verify_response()); } - // The secret goes in the POST body, never in the URL - public function testRequestSendsTheSecretByPost(): void { + // The API key goes in a header, never in the URL or the JSON body + public function testRequestCreatesAnEnterpriseAssessment(): void { FakeRecaptcha::install(); $_SERVER['REMOTE_ADDR'] = '198.51.100.7'; $this->request(array(), array('g-recaptcha-response' => 'token-from-widget')); captcha_verify_response(); $this->assertCount(1, FakeRecaptcha::$requests); $request = FakeRecaptcha::$requests[0]; - $this->assertSame('https://www.google.com/recaptcha/api/siteverify', $request['url']); + $this->assertSame('https://recaptchaenterprise.googleapis.com/v1/projects/test-project/assessments', $request['url']); $this->assertSame('POST', $request['http']['method']); - $this->assertSame(array('secret' => 'test-secret', 'response' => 'token-from-widget', 'remoteip' => '198.51.100.7'), $request['post']); - $this->assertStringContainsString('application/x-www-form-urlencoded', $request['http']['header']); + $this->assertSame(array('event' => array('token' => 'token-from-widget', 'siteKey' => 'test-site-key', + 'expectedAction' => 'submit', 'userIpAddress' => '198.51.100.7')), $request['post']); + $this->assertStringContainsString('Content-Type: application/json', $request['http']['header']); + $this->assertStringContainsString('X-Goog-Api-Key: test-api-key', $request['http']['header']); + $this->assertStringNotContainsString('test-api-key', $request['url'] . $request['http']['content']); $this->assertSame(10, $request['http']['timeout']); + $this->assertSame(0, $request['http']['follow_location']); + $this->assertFalse($request['http']['ignore_errors']); } public function testUnsolvedCaptchaFails(): void { - FakeRecaptcha::install('{"success": false, "error-codes": ["invalid-input-response"]}'); + FakeRecaptcha::install('{"tokenProperties":{"valid":false,"invalidReason":"DUPE"}}'); $this->request(array(), array('g-recaptcha-response' => 'bad')); $this->assertFalse(captcha_verify_response()); } public function testOddRepliesFail(): void { $this->request(array(), array('g-recaptcha-response' => 'token')); - foreach (array('', 'not json', '[]', '"success"', '{"success": 0}', '{}') as $reply) { + foreach (array('', 'not json', '[]', '"success"', 'null', 'true', '{"success":true}', '{}', + '{"tokenProperties":[]}', '{"tokenProperties":true}', '{"tokenProperties":"valid"}', + '{"tokenProperties":{}}') as $reply) { FakeRecaptcha::install($reply); - $this->assertFalse(captcha_verify_response(), $reply); + $this->assertFalse(captcha_check(), $reply); + $this->assertArrayNotHasKey('captcha_ok', $_SESSION); } } + public function testOnlyBooleanTrueIsAValidToken(): void { + $this->request(array(), array('g-recaptcha-response' => 'token')); + foreach (array(FALSE, NULL, 0, 1, 'true', 'false', array(TRUE)) as $valid) { + FakeRecaptcha::install(json_encode(array('tokenProperties' => array('valid' => $valid, 'hostname' => 'site.test', 'action' => 'submit')))); + $this->assertFalse(captcha_verify_response()); + } + } + + public function testHostnameMustMatchConfigurationNotTheRequestHost(): void { + $this->request(array(), array('g-recaptcha-response' => 'token')); + $_SERVER['HTTP_HOST'] = 'evil.example'; + try { + foreach (array(NULL, '', array('site.test'), 'evil.example', 'sub.site.test', 'site.test.evil.example') as $host) { + FakeRecaptcha::install(json_encode(array('tokenProperties' => array('valid' => TRUE, 'hostname' => $host, 'action' => 'submit')))); + $this->assertFalse(captcha_check()); + $this->assertArrayNotHasKey('captcha_ok', $_SESSION); + } + FakeRecaptcha::install(); + $this->assertTrue(captcha_verify_response()); + } finally { + unset($_SERVER['HTTP_HOST']); + } + } + + public function testHostnameIgnoresCaseAndTheConfiguredPort(): void { + $this->setEnv('SITE_URL', 'https://SITE.TEST:8443'); + FakeRecaptcha::install(); + $this->request(array(), array('g-recaptcha-response' => 'token')); + $this->assertTrue(captcha_verify_response()); + } + + public function testMissingHostnameFails(): void { + FakeRecaptcha::install('{"tokenProperties":{"valid":true,"action":"submit"}}'); + $this->request(array(), array('g-recaptcha-response' => 'token')); + $this->assertFalse(captcha_verify_response()); + } + + public function testActionMustMatchThePage(): void { + $this->request(array(), array('g-recaptcha-response' => 'token', 'action' => 'login')); + foreach (array(NULL, '', 'login', array('submit')) as $action) { + FakeRecaptcha::install(json_encode(array('tokenProperties' => array('valid' => TRUE, 'hostname' => 'site.test', 'action' => $action)))); + $this->assertFalse(captcha_check()); + $this->assertArrayNotHasKey('captcha_ok', $_SESSION); + } + FakeRecaptcha::install('{"tokenProperties":{"valid":true,"hostname":"site.test"}}'); + $this->assertFalse(captcha_verify_response()); + } + + public function testLoginHasItsOwnAction(): void { + FakeRecaptcha::install('{"tokenProperties":{"valid":true,"hostname":"site.test","action":"login"}}'); + $this->request(array(), array('g-recaptcha-response' => 'token')); + $this->assertTrue(captcha_verify_response('login')); + $this->assertSame('login', FakeRecaptcha::$requests[0]['post']['event']['expectedAction']); + $this->assertFalse(captcha_verify_response()); + } + + public function testALowScoreDoesNotChangeCheckboxBehavior(): void { + $reply = json_decode(FakeRecaptcha::PASS, TRUE); + $reply['riskAnalysis'] = array('score' => 0.1); + FakeRecaptcha::install(json_encode($reply)); + $this->request(array(), array('g-recaptcha-response' => 'token')); + $this->assertTrue(captcha_verify_response()); + } + public function testUnreachableServerFailsAndIsLogged(): void { FakeRecaptcha::install(FALSE); $this->request(array(), array('g-recaptcha-response' => 'token')); - $this->assertFalse(captcha_verify_response()); - $this->assertStringContainsString('xxx: could not reach the reCAPTCHA server', $this->loggedErrors()); + $this->assertFalse(captcha_check()); + $this->assertArrayNotHasKey('captcha_ok', $_SESSION); + $this->assertStringContainsString('xxx: reCAPTCHA assessment request failed', $this->loggedErrors()); } - public function testMissingSecretFailsAndIsLogged(): void { - $this->setEnv('RECAPTCHA_SECRET', ''); + public function testApiErrorsFailWithoutLoggingSecretsOrTheReply(): void { + $this->request(array(), array('g-recaptcha-response' => 'private-token')); + foreach (array(400, 401, 403, 429, 500, 503) as $code) { + // Reject an error even if the response also claims to have a valid token + $reply = json_decode(FakeRecaptcha::PASS, TRUE); + $reply['error'] = array('code' => $code, 'message' => 'test-api-key private-token'); + FakeRecaptcha::install(json_encode($reply)); + $this->assertFalse(captcha_check()); + $this->assertArrayNotHasKey('captcha_ok', $_SESSION); + } + $log = $this->loggedErrors(); + $this->assertStringContainsString('API error', $log); + $this->assertStringNotContainsString('test-api-key', $log); + $this->assertStringNotContainsString('private-token', $log); + } + + public static function requiredSettings(): array { + return array(array('RECAPTCHA_PROJECT_ID'), array('RECAPTCHA_API_KEY'), array('SITE_URL')); + } + + #[DataProvider('requiredSettings')] + public function testMissingConfigurationFailsAndIsLogged(string $name): void { + $this->setEnv($name, ''); FakeRecaptcha::install(); $this->request(array(), array('g-recaptcha-response' => 'token')); $this->assertFalse(captcha_verify_response()); $this->assertSame(array(), FakeRecaptcha::$requests); - $this->assertStringContainsString('RECAPTCHA_SECRET is not set', $this->loggedErrors()); + $this->assertStringContainsString($name, $this->loggedErrors()); + } + + public function testAnApiKeyCannotInjectHeaders(): void { + $this->setEnv('RECAPTCHA_API_KEY', "test-api-key\r\nInjected: value"); + FakeRecaptcha::install(); + $this->request(array(), array('g-recaptcha-response' => 'token')); + $this->assertFalse(captcha_verify_response()); + $this->assertSame(array(), FakeRecaptcha::$requests); + } + + public function testInvalidUtf8InTheTokenMakesNoRequest(): void { + FakeRecaptcha::install(); + $this->request(array(), array('g-recaptcha-response' => "\xff")); + $this->assertFalse(captcha_verify_response()); + $this->assertSame(array(), FakeRecaptcha::$requests); } public function testPassLastsHalfAnHour(): void { diff --git a/tests/php/Unit/ValidateDataTest.php b/tests/php/Unit/ValidateDataTest.php index 248adb9..b0ac3d5 100644 --- a/tests/php/Unit/ValidateDataTest.php +++ b/tests/php/Unit/ValidateDataTest.php @@ -35,24 +35,24 @@ class ValidateDataTest extends TestCase { } public function testVisitorsNeedTheCaptcha(): void { - FakeRecaptcha::install('{"success": false}'); + FakeRecaptcha::install('{"tokenProperties":{"valid":false}}'); $this->assertSame(array('captcha' => 'Please complete the captcha.'), $this->validate(array('g-recaptcha-response' => 'r'), FALSE)); } public function testASolvedCaptchaIsRemembered(): void { - FakeRecaptcha::install('{"success": true}'); + FakeRecaptcha::install(); $this->assertSame(array(), $this->validate(array('g-recaptcha-response' => 'r'), FALSE)); $this->assertGreaterThan(time() - 5, $_SESSION['captcha_ok']); } public function testAnOldCaptchaPassDoesNotCount(): void { - FakeRecaptcha::install('{"success": false}'); + FakeRecaptcha::install('{"tokenProperties":{"valid":false}}'); $_SESSION['captcha_ok'] = time() - CAPTCHA_PASS_SECONDS - 1; $this->assertArrayHasKey('captcha', $this->validate(array(), FALSE)); } public function testEditorsNeedNoCaptcha(): void { - FakeRecaptcha::install('{"success": false}'); + FakeRecaptcha::install('{"tokenProperties":{"valid":false}}'); $this->asEditor(); $this->assertSame(array(), $this->validate(array(), FALSE)); $this->assertSame(array(), FakeRecaptcha::$requests); diff --git a/tests/php/fixtures/router.php b/tests/php/fixtures/router.php index 8c80e2a..635813b 100644 --- a/tests/php/fixtures/router.php +++ b/tests/php/fixtures/router.php @@ -41,8 +41,18 @@ function xxx_page_route() { file_put_contents("$state/tables.ser", serialize(array('tables' => SiteData::$tables, 'passwordLength' => SiteData::$passwordLength)), LOCK_EX); }); - FakeRecaptcha::install(function ($post) { - return json_encode(array('success' => ($post['response'] ?? '') === 'pass')); + FakeRecaptcha::install(function ($post) use ($file) { + $token = $post['event']['token'] ?? ''; + if ($token === 'unreachable') + return FALSE; + if ($token === 'api-error') + return '{"error":{"code":403,"status":"PERMISSION_DENIED"}}'; + $action = $file === XXX_ROOT . '/publish/login.php' ? 'login' : 'submit'; + return json_encode(array('tokenProperties' => array( + 'valid' => in_array($token, array('pass', 'wrong-host', 'wrong-action'), TRUE), + 'hostname' => $token === 'wrong-host' ? 'evil.example' : 'site.test', + 'action' => $token === 'wrong-action' ? 'other' : $action, + ))); }); if (!isset($_SERVER['HTTP_X_TEST_PLAIN'])) diff --git a/tests/php/fixtures/test.env b/tests/php/fixtures/test.env index 8bf3bf5..5c387ef 100644 --- a/tests/php/fixtures/test.env +++ b/tests/php/fixtures/test.env @@ -5,7 +5,8 @@ DB_PASS=test-pass DB_NAME=test-db SITE_URL=https://site.test RECAPTCHA_SITE_KEY=test-site-key -RECAPTCHA_SECRET=test-secret +RECAPTCHA_PROJECT_ID=test-project +RECAPTCHA_API_KEY=test-api-key GOOGLE_ANALYTICS_ID= UPLOAD_DIR= UPLOAD_DIR_LIMIT_MB=1