From 1468cf7138c6449b2047f76d85ad693b94cf2513 Mon Sep 17 00:00:00 2001 From: Timothy Allen Date: Fri, 25 Sep 2026 21:54:14 +0200 Subject: [PATCH] Require a captcha for visitor submissions and for login. Without the form field, the check was skipped. A solved captcha lasts 30 minutes in the session. --- captcha.lib.php | 72 +++++++++++++++++++++++++++++++++++++++++ publish/index.php | 2 +- publish/login.php | 9 ++++++ publish/post.lib.php | 10 +++++- publish/preview.inc.php | 19 ----------- publish/style.css | 4 +-- publish/write.inc.php | 5 +-- session.lib.php | 1 + 8 files changed, 97 insertions(+), 25 deletions(-) create mode 100644 captcha.lib.php diff --git a/captcha.lib.php b/captcha.lib.php new file mode 100644 index 0000000..8d43894 --- /dev/null +++ b/captcha.lib.php @@ -0,0 +1,72 @@ +'; +} + +function captcha_widget() { + return '
'; +} + +// Ask Google whether the captcha in this POST was solved +function captcha_verify_response() { + $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'); + 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, + ))); + $reply = @file_get_contents('https://www.google.com/recaptcha/api/siteverify', FALSE, $context); + if ($reply === FALSE) { + error_log('xxx: could not reach the reCAPTCHA server'); + return FALSE; + } + $result = json_decode($reply); + return is_object($result) && !empty($result->success); +} + +/* + * For story submissions: a solved captcha counts for CAPTCHA_PASS_SECONDS + * in this session, so the preview and the submit need only one captcha. + * captcha_used() ends the pass once a story is saved. + */ +function captcha_passed() { + return !empty($_SESSION['captcha_ok']) && $_SESSION['captcha_ok'] > time() - CAPTCHA_PASS_SECONDS; +} + +function captcha_check() { + if (captcha_passed()) + return TRUE; + if (captcha_verify_response()) { + $_SESSION['captcha_ok'] = time(); + return TRUE; + } + return FALSE; +} + +function captcha_used() { + unset($_SESSION['captcha_ok']); +} +?> diff --git a/publish/index.php b/publish/index.php index ea4774d..10acb40 100644 --- a/publish/index.php +++ b/publish/index.php @@ -19,7 +19,7 @@ require_once("../session.lib.php"); - + 0) { $denied = TRUE; $wait_minutes = (int)ceil($wait / 60); +} else if (isset($_POST['login']) && !captcha_verify_response()) { + $denied = TRUE; + $captcha_failed = TRUE; } else if (isset($_POST['login'])) { include_once('../open_db.lib.php'); $table = "editors"; @@ -76,6 +80,7 @@ if (isset($_SESSION['editor'])) { + @@ -89,6 +94,8 @@ if (isset($_SESSION['editor'])) { Too many failed logins. Please try again in $wait_minutes minute".($wait_minutes == 1 ? "" : "s").".

\n"; +else if ($captcha_failed) + echo "

Please complete the captcha.

\n"; else if ($denied) echo "

Access denied. Please try again.

\n"; ?> @@ -112,6 +119,8 @@ else if ($denied) + +
diff --git a/publish/post.lib.php b/publish/post.lib.php index 1a91ac3..3addd41 100644 --- a/publish/post.lib.php +++ b/publish/post.lib.php @@ -115,6 +115,10 @@ function save_data($id = NULL) { if (!$is_admin && $action == 'submit') $_SESSION['allow_queue_image'] = $id; + // A visitor's next story needs a new captcha + if (!$is_admin) + captcha_used(); + // Prune filesystem if ($image_file) prune_images($image_file); @@ -189,7 +193,7 @@ function prune_images($image_file) { */ $validate = array(); function validate_data() { - global $validate; + global $validate, $is_admin; // extract POST variables $result = get_post_data(TRUE); // assign their keys and values to the symbol table @@ -197,6 +201,10 @@ function validate_data() { $upload_error = $_FILES['upload']['error'] ?? UPLOAD_ERR_NO_FILE; $upload_tmp = $_FILES['upload']['tmp_name'] ?? ''; + // Visitors must solve the captcha before a preview or a save + if (!$is_admin && !captcha_check()) { + $validate['captcha'] = 'Please complete the captcha.'; + } if (!$author) { $validate['author'] = "You have not submitted an author."; diff --git a/publish/preview.inc.php b/publish/preview.inc.php index 378fb84..35f9ccb 100644 --- a/publish/preview.inc.php +++ b/publish/preview.inc.php @@ -2,25 +2,6 @@ $result = get_post_data(TRUE); extract($result[0], EXTR_OVERWRITE); -if (isset($_POST['g-recaptcha-response'])) { - $captcha = $_POST['g-recaptcha-response']; - if (! $captcha) { - $validate['captcha'] = 'Invalid captcha. Please try again.'; - include('./write.inc.php'); - return; - } - $captcha_resp = file_get_contents("https://www.google.com/recaptcha/api/siteverify?secret=".urlencode(config('RECAPTCHA_SECRET'))."&response=". - $captcha."&remoteip=".$_SERVER['REMOTE_ADDR']); - var_dump($captcha_resp); - // PHP 8 treats the bare word "success" as an undefined constant, so decode the JSON reply - $captcha_json = json_decode((string)$captcha_resp); - if (empty($captcha_json->success)) { - $validate['captcha'] = 'The words you typed in the captcha were not correct. Please try again.'; - include('./write.inc.php'); - return; - } -} - echo '

Please review your submission. Once you\'ve finished, please click "Submit" at the bottom of the page, or click "Preview" to check it again.

'; // Keep a new image outside the web root until the story is saved; diff --git a/publish/style.css b/publish/style.css index e7f1108..3dfa5b4 100644 --- a/publish/style.css +++ b/publish/style.css @@ -148,8 +148,8 @@ ul.error-box li { } #main .grid div.g-recaptcha { - overflow: inherit; - margin-left: 120px; + grid-column: 2; + margin: 5px 0; } #captcha #recaptcha_image { margin-left: 120px; diff --git a/publish/write.inc.php b/publish/write.inc.php index 8ef0548..14fa83f 100644 --- a/publish/write.inc.php +++ b/publish/write.inc.php @@ -103,8 +103,9 @@ if (isset($file_src) || isset($img)) {
'; + // Visitors solve the captcha once; the session remembers it until the story is saved + if (!$is_admin && !captcha_passed()) { + echo captcha_widget(); } if ($is_admin) { diff --git a/session.lib.php b/session.lib.php index 72422d2..a56296e 100644 --- a/session.lib.php +++ b/session.lib.php @@ -1,6 +1,7 @@