From 81ad1fff389c919fd0b7de35a070dcd9bddec87d Mon Sep 17 00:00:00 2001 From: Zankaria Date: Mon, 28 Oct 2024 00:44:26 +0100 Subject: [PATCH 01/11] post_form: add unified captcha response hook --- templates/post_form.html | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/post_form.html b/templates/post_form.html index 44327915..27c8741b 100644 --- a/templates/post_form.html +++ b/templates/post_form.html @@ -118,6 +118,7 @@
+ {{ antibot.html() }} From 2319a7b74e3fba770835333a3fa53abe299aeacc Mon Sep 17 00:00:00 2001 From: Zankaria Date: Mon, 28 Oct 2024 00:45:50 +0100 Subject: [PATCH 02/11] post.php: use unified captcha-response --- post.php | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/post.php b/post.php index ae1361ce..232a1f8e 100644 --- a/post.php +++ b/post.php @@ -768,25 +768,25 @@ function handle_post() if ($config['dynamic_captcha'] !== false) { if ($_SERVER['REMOTE_ADDR'] === $config['dynamic_captcha']) { if ($config['recaptcha']) { - if (!isset($_POST['g-recaptcha-response'])) { + if (!isset($_POST['captcha-response'])) { error($config['error']['bot']); } - if (!check_recaptcha($config['recaptcha_private'], $_POST['g-recaptcha-response'], null)) { + if (!check_recaptcha($config['recaptcha_private'], $_POST['captcha-response'], null)) { error($config['error']['captcha']); } } elseif ($config['hcaptcha']) { - if (!isset($_POST['h-captcha-response'])) { + if (!isset($_POST['captcha-response'])) { error($config['error']['bot']); } - if (!check_hcaptcha($config['hcaptcha_private'], $_POST['h-captcha-response'], null, $config['hcaptcha_public'])) { + if (!check_hcaptcha($config['hcaptcha_private'], $_POST['captcha-response'], null, $config['hcaptcha_public'])) { error($config['error']['captcha']); } } elseif ($config['turnstile']) { - if (!isset($_POST['cf-turnstile-response'])) { + if (!isset($_POST['captcha-response'])) { error($config['error']['bot']); } $expected_action = $post['op'] ? 'post-thread' : 'post-reply'; - if (!check_turnstile($config['turnstile_private'], $_POST['cf-turnstile-response'], null, $expected_action)) { + if (!check_turnstile($config['turnstile_private'], $_POST['captcha-response'], null, $expected_action)) { error($config['error']['captcha']); } } @@ -794,25 +794,25 @@ function handle_post() } else { // Check for CAPTCHA right after opening the board so the "return" link is in there. if ($config['recaptcha']) { - if (!isset($_POST['g-recaptcha-response'])) { + if (!isset($_POST['captcha-response'])) { error($config['error']['bot']); } - if (!check_recaptcha($config['recaptcha_private'], $_POST['g-recaptcha-response'], $_SERVER['REMOTE_ADDR'])) { + if (!check_recaptcha($config['recaptcha_private'], $_POST['captcha-response'], $_SERVER['REMOTE_ADDR'])) { error($config['error']['captcha']); } } elseif ($config['hcaptcha']) { - if (!isset($_POST['h-captcha-response'])) { + if (!isset($_POST['captcha-response'])) { error($config['error']['bot']); } - if (!check_hcaptcha($config['hcaptcha_private'], $_POST['h-captcha-response'], $_SERVER['REMOTE_ADDR'], $config['hcaptcha_public'])) { + if (!check_hcaptcha($config['hcaptcha_private'], $_POST['captcha-response'], $_SERVER['REMOTE_ADDR'], $config['hcaptcha_public'])) { error($config['error']['captcha']); } } elseif ($config['turnstile']) { - if (!isset($_POST['cf-turnstile-response'])) { + if (!isset($_POST['captcha-response'])) { error($config['error']['bot']); } $expected_action = $post['op'] ? 'post-thread' : 'post-reply'; - if (!check_turnstile($config['turnstile_private'], $_POST['cf-turnstile-response'], null, $expected_action)) { + if (!check_turnstile($config['turnstile_private'], $_POST['captcha-response'], null, $expected_action)) { error($config['error']['captcha']); } } From 93e37b0c423a645715423fc4e575b89dc7bbcbf9 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Mon, 28 Oct 2024 00:48:12 +0100 Subject: [PATCH 03/11] main.js: use unified captcha-response --- templates/main.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/templates/main.js b/templates/main.js index 26505f7e..cba5a25a 100755 --- a/templates/main.js +++ b/templates/main.js @@ -273,8 +273,9 @@ var captcha_renderer = null; function onCaptchaLoadHcaptcha() { if (captcha_renderer === null && (active_page === 'index' || active_page === 'catalog' || active_page === 'thread')) { let renderer = { - renderOn: (container) => hcaptcha.render(container, { + applyOn: (container, params) => hcaptcha.render(container, { sitekey: "{{ config.hcaptcha_public }}", + callback: params['on-success'], }), remove: (widgetId) => { /* Not supported */ }, reset: (widgetId) => hcaptcha.reset(widgetId) @@ -300,10 +301,11 @@ window.onCaptchaLoadTurnstile_post_thread = function() { function onCaptchaLoadTurnstile(action) { if (captcha_renderer === null && (active_page === 'index' || active_page === 'catalog' || active_page === 'thread')) { let renderer = { - renderOn: function(container) { + applyOn: function(container, params) { let widgetId = turnstile.render('#' + container, { sitekey: "{{ config.turnstile_public }}", action: action, + callback: params['on-success'], }); if (widgetId === undefined) { return null; @@ -322,7 +324,11 @@ function onCaptchaLoadTurnstile(action) { function onCaptchaLoad(renderer) { captcha_renderer = renderer; - let widgetId = renderer.renderOn('captcha-container'); + let widgetId = renderer.applyOn('captcha-container', { + 'on-success': function(token) { + document.getElementById('captcha-response').value = token; + } + }); if (widgetId === null) { console.error('Could not render captcha!'); } From 6fdc16d2f3637ed103099af089ddec2286db19c4 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Mon, 28 Oct 2024 11:17:01 +0100 Subject: [PATCH 04/11] config.php: accept unified captcha-response --- inc/config.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/inc/config.php b/inc/config.php index 121542ab..7609feba 100644 --- a/inc/config.php +++ b/inc/config.php @@ -301,9 +301,7 @@ 'lock', 'raw', 'embed', - 'g-recaptcha-response', - 'h-captcha-response', - 'cf-turnstile-response', + 'captcha-response', 'spoiler', 'page', 'file_url', From 04ef9614408c8334c3d80b00f0c9e32fdf7cccbb Mon Sep 17 00:00:00 2001 From: Zankaria Date: Mon, 28 Oct 2024 11:24:57 +0100 Subject: [PATCH 05/11] config.php: refactor captcha configuration --- inc/config.php | 59 +++++++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/inc/config.php b/inc/config.php index 7609feba..348b6616 100644 --- a/inc/config.php +++ b/inc/config.php @@ -328,33 +328,38 @@ 'answer' => '4' ); */ - /** - * The captcha is dynamically injected on the client if the server replies with the `captcha-required` cookie set - * to 1. - * Use false to disable this configuration, otherwise, set the IP that vichan should check for captcha responses. - */ - $config['dynamic_captcha'] = false; - - // Enable reCaptcha to make spam even harder. Rarely necessary. - $config['recaptcha'] = false; - - // Public and private key pair from https://www.google.com/recaptcha/admin/create - $config['recaptcha_public'] = '6LcXTcUSAAAAAKBxyFWIt2SO8jwx4W7wcSMRoN3f'; - $config['recaptcha_private'] = '6LcXTcUSAAAAAOGVbVdhmEM1_SyRF4xTKe8jbzf_'; - - // Enable hCaptcha. - $config['hcaptcha'] = false; - - // Public and private key pair for using hCaptcha. - $config['hcaptcha_public'] = '7a4b21e0-dc53-46f2-a9f8-91d2e74b63a0'; - $config['hcaptcha_private'] = '0x4e9A01bE637b51dC41a7Ea9865C3fDe4aB72Cf17'; - - // Enable Cloudflare's Turnstile captcha. - $config['turnstile'] = false; - - // Public and private key pair for turnstile. - $config['turnstile_public'] = ''; - $config['turnstile_private'] = ''; + // Enable a captcha system to make spam even harder. Rarely necessary. + $config['captcha'] = [ + /** + * Select the captcha backend, false to disable. + * Can be false, "recaptcha", "hcaptcha" or "turnstile". + */ + 'mode' => false, + /** + * The captcha is dynamically injected on the client if the server replies with the `captcha-required` cookie set + * to 1. + * Use false to disable this configuration, otherwise, set the IP that vichan should check for captcha responses. + */ + 'dynamic' => false, + // Configure Google reCAPTCHA. + 'recaptcha' => [ + // Public and private key pair from https://www.google.com/recaptcha/admin/create + 'public' => '6LcXTcUSAAAAAKBxyFWIt2SO8jwx4W7wcSMRoN3f', + 'private' => '6LcXTcUSAAAAAOGVbVdhmEM1_SyRF4xTKe8jbzf_', + ], + // Configure hCaptcha. + 'hcaptcha' => [ + // Public and private key pair for using hCaptcha. + 'public' => '7a4b21e0-dc53-46f2-a9f8-91d2e74b63a0', + 'private' => '0x4e9A01bE637b51dC41a7Ea9865C3fDe4aB72Cf17', + ], + // Configure Cloudflare Turnstile. + 'turnstile' => [ + // Public and private key pair for turnstile. + 'public' => '', + 'private' => '', + ] + ]; // Ability to lock a board for normal users and still allow mods to post. Could also be useful for making an archive board $config['board_locked'] = false; From 2741b1ea056c6f3eb9f08bb79413b59f7acc9b29 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Mon, 28 Oct 2024 12:32:48 +0100 Subject: [PATCH 06/11] config.php: accept captcha-form-id --- inc/config.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/inc/config.php b/inc/config.php index 348b6616..f3e041d0 100644 --- a/inc/config.php +++ b/inc/config.php @@ -302,6 +302,7 @@ 'raw', 'embed', 'captcha-response', + 'captcha-form-id', 'spoiler', 'page', 'file_url', @@ -341,6 +342,8 @@ * Use false to disable this configuration, otherwise, set the IP that vichan should check for captcha responses. */ 'dynamic' => false, + // Require to be non-zero if you use js/ajax.js (preferably no more than a few seconds), otherwise weird errors might occur. + 'passthrough_timeout' => 0, // Configure Google reCAPTCHA. 'recaptcha' => [ // Public and private key pair from https://www.google.com/recaptcha/admin/create From f792470af72bc569eb942e2c4a19feb0d28638e9 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Mon, 28 Oct 2024 12:33:19 +0100 Subject: [PATCH 07/11] post_form.html: add captcha form id --- templates/post_form.html | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/post_form.html b/templates/post_form.html index 27c8741b..adca9ef6 100644 --- a/templates/post_form.html +++ b/templates/post_form.html @@ -119,6 +119,7 @@
+ {{ antibot.html() }} From ebc0c54657780b22fd5a740cbb2279668e66a906 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Mon, 28 Oct 2024 12:33:36 +0100 Subject: [PATCH 08/11] main.js: generate captcha form id --- templates/main.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/templates/main.js b/templates/main.js index cba5a25a..d86d5b70 100755 --- a/templates/main.js +++ b/templates/main.js @@ -322,6 +322,9 @@ function onCaptchaLoadTurnstile(action) { {% endif %} // End if turnstile function onCaptchaLoad(renderer) { + // Initialize the form identifier with a random password. + document.getElementById('captcha-form-id').value = generatePassword(); + captcha_renderer = renderer; let widgetId = renderer.applyOn('captcha-container', { From a7e349d8cbb34676854c81087791b643db743b75 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Mon, 28 Oct 2024 12:34:05 +0100 Subject: [PATCH 09/11] post.php: workaround captcha and js/ajax.js bug --- post.php | 120 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 71 insertions(+), 49 deletions(-) diff --git a/post.php b/post.php index 232a1f8e..b16b7847 100644 --- a/post.php +++ b/post.php @@ -167,6 +167,68 @@ function check_turnstile($secret, $response, $remote_ip, $expected_action) return $json_ret['success'] === true && $json_ret['action'] === $expected_action; } +/** + * A "sophisticated" workaround to js/ajax.js calling post.php multiple times on error/ban. + */ +function check_captcha(array $captcha_config, string $form_id, string $board_uri, string $response, string $remote_ip, string $expected_action) { + $dynamic = $captcha_config['dynamic']; + if ($dynamic !== false && $remote_ip !== $dynamic) { + return true; + } + + switch ($captcha_config['mode']) { + case 'recaptcha': + case 'hcaptcha': + case 'turnstile': + $mode = $captcha_config['mode']; + break; + case false: + return true; + default: + \error_log("Unknown captcha mode '{$captcha_config['mode']}'"); + throw new \RuntimeException('Captcha configuration error'); + } + + $passthrough_timeout = $captcha_config['passthrough_timeout']; + + if ($passthrough_timeout != 0) { + $pass = Cache::get("captcha_passthrough_{$remote_ip}_{$form_id}"); + if ($pass !== false) { + $let_through = $pass['expires'] > time() && $pass['board_uri'] === $board_uri && $pass['captcha_response'] === $response; + if ($let_through) { + return true; + } + } + } + + $remote_ip_send = $dynamic !== false ? null : $remote_ip; + $private_key = $captcha_config[$mode]['private']; + $public_key = $captcha_config[$mode]['public']; + switch ($mode) { + case 'recaptcha': + $ret = check_recaptcha($private_key, $response, $remote_ip_send); + break; + case 'hcaptcha': + $ret = check_hcaptcha($private_key, $response, $remote_ip_send, $public_key); + break; + case 'turnstile': + $ret = check_turnstile($private_key, $response, $remote_ip_send, $expected_action); + break; + } + + if ($ret && $passthrough_timeout != 0) { + $pass = [ + 'expires' => time() + $passthrough_timeout, + 'board_uri' => $board_uri, + 'captcha_response' => $response + ]; + + Cache::set("captcha_passthrough_{$remote_ip}_{$form_id}", $pass, $passthrough_timeout + 2); + } + + return $ret; +} + /** * Deletes the (single) captcha associated with the ip and code. * @@ -765,56 +827,16 @@ function handle_post() if (!$dropped_post) { - if ($config['dynamic_captcha'] !== false) { - if ($_SERVER['REMOTE_ADDR'] === $config['dynamic_captcha']) { - if ($config['recaptcha']) { - if (!isset($_POST['captcha-response'])) { - error($config['error']['bot']); - } - if (!check_recaptcha($config['recaptcha_private'], $_POST['captcha-response'], null)) { - error($config['error']['captcha']); - } - } elseif ($config['hcaptcha']) { - if (!isset($_POST['captcha-response'])) { - error($config['error']['bot']); - } - if (!check_hcaptcha($config['hcaptcha_private'], $_POST['captcha-response'], null, $config['hcaptcha_public'])) { - error($config['error']['captcha']); - } - } elseif ($config['turnstile']) { - if (!isset($_POST['captcha-response'])) { - error($config['error']['bot']); - } - $expected_action = $post['op'] ? 'post-thread' : 'post-reply'; - if (!check_turnstile($config['turnstile_private'], $_POST['captcha-response'], null, $expected_action)) { - error($config['error']['captcha']); - } - } + // Check for CAPTCHA right after opening the board so the "return" link is in there. + if ($config['captcha']['mode'] !== false) { + if (!isset($_POST['captcha-response'], $_POST['captcha-form-id'])) { + error($config['error']['bot']); } - } else { - // Check for CAPTCHA right after opening the board so the "return" link is in there. - if ($config['recaptcha']) { - if (!isset($_POST['captcha-response'])) { - error($config['error']['bot']); - } - if (!check_recaptcha($config['recaptcha_private'], $_POST['captcha-response'], $_SERVER['REMOTE_ADDR'])) { - error($config['error']['captcha']); - } - } elseif ($config['hcaptcha']) { - if (!isset($_POST['captcha-response'])) { - error($config['error']['bot']); - } - if (!check_hcaptcha($config['hcaptcha_private'], $_POST['captcha-response'], $_SERVER['REMOTE_ADDR'], $config['hcaptcha_public'])) { - error($config['error']['captcha']); - } - } elseif ($config['turnstile']) { - if (!isset($_POST['captcha-response'])) { - error($config['error']['bot']); - } - $expected_action = $post['op'] ? 'post-thread' : 'post-reply'; - if (!check_turnstile($config['turnstile_private'], $_POST['captcha-response'], null, $expected_action)) { - error($config['error']['captcha']); - } + + $expected_action = $post['op'] ? 'post-thread' : 'post-reply'; + $ret = check_captcha($config['captcha'], $_POST['captcha-form-id'], $post['board'], $_POST['captcha-response'], $_SERVER['REMOTE_ADDR'], $expected_action); + if (!$ret) { + error($config['error']['captcha']); } } From 777c3b628a2198d5e3bfc427e6bf908196838cde Mon Sep 17 00:00:00 2001 From: Zankaria Date: Mon, 28 Oct 2024 13:15:39 +0100 Subject: [PATCH 10/11] cache.php: fix typo --- inc/cache.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/cache.php b/inc/cache.php index a8428846..de85ecd1 100644 --- a/inc/cache.php +++ b/inc/cache.php @@ -99,7 +99,7 @@ class Cache { case 'redis': if (!self::$cache) self::init(); - self::$cache->setex($key, $expires, json_encode($value)); + self::$cache->setEx($key, $expires, json_encode($value)); break; case 'apc': apc_store($key, $value, $expires); From 00d7073c2545326c8c7f26231bf0805f0a74a0f4 Mon Sep 17 00:00:00 2001 From: Zankaria Date: Mon, 28 Oct 2024 13:32:25 +0100 Subject: [PATCH 11/11] cache.php: fix redis value decoding --- inc/cache.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/inc/cache.php b/inc/cache.php index de85ecd1..3f1b8cb1 100644 --- a/inc/cache.php +++ b/inc/cache.php @@ -73,7 +73,8 @@ class Cache { case 'redis': if (!self::$cache) self::init(); - $data = json_decode(self::$cache->get($key), true); + $ret = self::$cache->get($key); + $data = $ret !== false ? json_decode($ret, true) : false; break; }