Use the reCAPTCHA Enterprise API.
This commit is contained in:
+55
-26
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
/*
|
||||
* Google reCAPTCHA v2. The keys come from RECAPTCHA_SITE_KEY and
|
||||
* RECAPTCHA_SECRET in .env.
|
||||
* Google reCAPTCHA Enterprise, with a checkbox key. Configuration comes
|
||||
* from RECAPTCHA_SITE_KEY, RECAPTCHA_PROJECT_ID and RECAPTCHA_API_KEY.
|
||||
* SITE_URL supplies the hostname that a response must match.
|
||||
*
|
||||
* Without RECAPTCHA_SITE_KEY the captcha is off: the pages load nothing
|
||||
* from Google and every check passes. With the site key but without the
|
||||
* secret, or when Google cannot be reached, every check fails (and the
|
||||
* reason is logged).
|
||||
* Without RECAPTCHA_SITE_KEY the captcha is off. With a site key, missing
|
||||
* configuration or a failed assessment blocks the request.
|
||||
*/
|
||||
require_once(__DIR__ . '/config.lib.php');
|
||||
require_once(__DIR__ . '/html.lib.php');
|
||||
@@ -20,44 +19,74 @@ function captcha_enabled() {
|
||||
function captcha_script() {
|
||||
if (!captcha_enabled())
|
||||
return '';
|
||||
return '<script src="https://www.google.com/recaptcha/api.js" async defer></script>';
|
||||
return '<script src="https://www.google.com/recaptcha/enterprise.js" async defer></script>';
|
||||
}
|
||||
|
||||
function captcha_widget() {
|
||||
function captcha_widget($action = 'submit') {
|
||||
if (!captcha_enabled())
|
||||
return '';
|
||||
return '<div class="g-recaptcha" data-sitekey="'.form_value(config('RECAPTCHA_SITE_KEY')).'"></div>';
|
||||
return '<div class="g-recaptcha" data-sitekey="'.form_value(config('RECAPTCHA_SITE_KEY')).'" data-action="'.form_value($action).'"></div>';
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Reference in New Issue
Block a user