Use the reCAPTCHA Enterprise API.
This commit is contained in:
+133
-17
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
namespace Xxx\Tests\Unit;
|
||||
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use Xxx\Tests\Support\FakeRecaptcha;
|
||||
use Xxx\Tests\Support\TestCase;
|
||||
|
||||
@@ -17,13 +18,19 @@ class CaptchaTest extends TestCase {
|
||||
}
|
||||
|
||||
public function testScriptAndWidget(): void {
|
||||
$this->assertSame('<script src="https://www.google.com/recaptcha/api.js" async defer></script>', captcha_script());
|
||||
$this->assertSame('<div class="g-recaptcha" data-sitekey="test-site-key"></div>', captcha_widget());
|
||||
$this->assertSame('<script src="https://www.google.com/recaptcha/enterprise.js" async defer></script>', captcha_script());
|
||||
$this->assertSame('<div class="g-recaptcha" data-sitekey="test-site-key" data-action="submit"></div>', captcha_widget());
|
||||
$this->assertSame('<div class="g-recaptcha" data-sitekey="test-site-key" data-action="login"></div>', captcha_widget('login'));
|
||||
}
|
||||
|
||||
public function testWidgetEscapesTheSiteKey(): void {
|
||||
$this->setEnv('RECAPTCHA_SITE_KEY', '"><script>x</script>');
|
||||
$this->assertSame('<div class="g-recaptcha" data-sitekey=""><script>x</script>"></div>', captcha_widget());
|
||||
$this->assertSame('<div class="g-recaptcha" data-sitekey=""><script>x</script>" data-action="submit"></div>', captcha_widget());
|
||||
}
|
||||
|
||||
public function testWidgetEscapesTheAction(): void {
|
||||
$this->assertSame('<div class="g-recaptcha" data-sitekey="test-site-key" data-action=""><script>x</script>"></div>',
|
||||
captcha_widget('"><script>x</script>'));
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user