Merge pull request #876 from perdedora/captcha-fixes

Captcha fixes
This commit is contained in:
Lorenzo Yario 2025-01-09 23:25:27 -06:00 committed by GitHub
commit 5a80c4ef22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 127 additions and 131 deletions

View File

@ -10,19 +10,16 @@ class SecureImageCaptchaQuery {
private HttpDriver $http; private HttpDriver $http;
private string $domain; private string $domain;
private string $provider_check; private string $provider_check;
private string $extra;
/** /**
* @param HttpDriver $http The http client. * @param HttpDriver $http The http client.
* @param string $domain The server's domain. * @param string $domain The server's domain.
* @param string $provider_check Path to the endpoint. * @param string $provider_check Path to the endpoint.
* @param string $extra Extra http parameters.
*/ */
function __construct(HttpDriver $http, string $domain, string $provider_check, string $extra) { function __construct(HttpDriver $http, string $domain, string $provider_check) {
$this->http = $http; $this->http = $http;
$this->domain = $domain; $this->domain = $domain;
$this->provider_check = $provider_check; $this->provider_check = $provider_check;
$this->extra = $extra;
} }
/** /**
@ -37,11 +34,12 @@ class SecureImageCaptchaQuery {
$data = [ $data = [
'mode' => 'check', 'mode' => 'check',
'text' => $user_text, 'text' => $user_text,
'extra' => $this->extra,
'cookie' => $user_cookie 'cookie' => $user_cookie
]; ];
$ret = $this->http->requestGet($this->domain . '/' . $this->provider_check, $data); $ret = $this->http->requestGet($this->domain . '/' . $this->provider_check, $data);
return $ret === '1'; $resp = \json_decode($ret, true, 16, \JSON_THROW_ON_ERROR);
return isset($resp['success']) && $resp['success'];
} }
} }

View File

@ -384,13 +384,18 @@
// To enable the native captcha you need to change a couple of settings. Read more at: /inc/captcha/readme.md // To enable the native captcha you need to change a couple of settings. Read more at: /inc/captcha/readme.md
'native' => [ 'native' => [
// Custom captcha get provider path (if not working get the absolute path aka your url). // Custom captcha get provider path (if not working get the absolute path aka your url).
'provider_get' => '../inc/captcha/entrypoint.php', 'provider_get' => 'securimage.php',
// Custom captcha check provider path // Custom captcha check provider path
'provider_check' => '../inc/captcha/entrypoint.php', 'provider_check' => 'securimage.php',
// Custom captcha extra field (eg. charset) // Custom captcha extra field (eg. charset)
'extra' => 'abcdefghijklmnopqrstuvwxyz', 'extra' => 'abcdefghijklmnopqrstuvwxyz',
// New thread captcha. Require solving a captcha to post a thread. // New thread captcha. Require solving a captcha to post a thread.
'new_thread_capt' => false 'new_thread_capt' => false,
// Securimage customization options
// https://github.com/dapphp/securimage/blob/nextgen/examples/securimage_show_example.php#L49
'securimage_options' => ['send_headers' => false, 'no_exit' => true],
// Captcha expires (in seconds)
'expires_in' => 320
] ]
]; ];

View File

@ -86,8 +86,7 @@ function build_context(array $config): Context {
return new SecureImageCaptchaQuery( return new SecureImageCaptchaQuery(
$c->get(HttpDriver::class), $c->get(HttpDriver::class),
$config['domain'], $config['domain'],
$config['captcha']['native']['provider_check'], $config['captcha']['native']['provider_check']
$config['captcha']['native']['extra']
); );
}, },
CacheDriver::class => function($c) { CacheDriver::class => function($c) {

View File

@ -1,27 +1,27 @@
var tout; var tout;
function redo_events(provider, extra) { function redo_events(provider) {
$('.captcha .captcha_text, textarea[id="body"]').off("focus").one("focus", function() { actually_load_captcha(provider, extra); }); $('.captcha .captcha_text, textarea[id="body"]').off("focus").one("focus", function() { actually_load_captcha(provider); });
} }
function actually_load_captcha(provider, extra) { function actually_load_captcha(provider) {
$('.captcha .captcha_text, textarea[id="body"]').off("focus"); $('.captcha .captcha_text, textarea[id="body"]').off("focus");
if (tout !== undefined) { if (tout !== undefined) {
clearTimeout(tout); clearTimeout(tout);
} }
$.getJSON(provider, {mode: 'get', extra: extra}, function(json) { $.getJSON(provider, {mode: 'get'}, function(json) {
$(".captcha .captcha_cookie").val(json.cookie); $(".captcha .captcha_cookie").val(json.cookie);
$(".captcha .captcha_html").html(json.captchahtml); $(".captcha .captcha_html").html(json.captchahtml);
setTimeout(function() { setTimeout(function() {
redo_events(provider, extra); redo_events(provider);
}, json.expires_in * 1000); }, json.expires_in * 1000);
}); });
} }
function load_captcha(provider, extra) { function load_captcha(provider) {
$(function() { $(function() {
$(".captcha>td").html("<input class='captcha_text' type='text' name='captcha_text' size='32' maxlength='6' autocomplete='off'>"+ $(".captcha>td").html("<input class='captcha_text' type='text' name='captcha_text' size='32' maxlength='6' autocomplete='off'>"+
"<input class='captcha_cookie' name='captcha_cookie' type='hidden'>"+ "<input class='captcha_cookie' name='captcha_cookie' type='hidden'>"+
@ -29,15 +29,15 @@ function load_captcha(provider, extra) {
$("#quick-reply .captcha .captcha_text").prop("placeholder", _("Verification")); $("#quick-reply .captcha .captcha_text").prop("placeholder", _("Verification"));
$(".captcha .captcha_html").on("click", function() { actually_load_captcha(provider, extra); }); $(".captcha .captcha_html").on("click", function() { actually_load_captcha(provider); });
$(document).on("ajax_after_post", function() { actually_load_captcha(provider, extra); }); $(document).on("ajax_after_post", function() { actually_load_captcha(provider); });
redo_events(provider, extra); redo_events(provider);
$(window).on("quick-reply", function() { $(window).on("quick-reply", function() {
redo_events(provider, extra); redo_events(provider);
$("#quick-reply .captcha .captcha_html").html($("form:not(#quick-reply) .captcha .captcha_html").html()); $("#quick-reply .captcha .captcha_html").html($("form:not(#quick-reply) .captcha .captcha_html").html());
$("#quick-reply .captcha .captcha_cookie").val($("form:not(#quick-reply) .captcha .captcha_cookie").html()); $("#quick-reply .captcha .captcha_cookie").val($("form:not(#quick-reply) .captcha .captcha_cookie").html());
$("#quick-reply .captcha .captcha_html").on("click", function() { actually_load_captcha(provider, extra); }); $("#quick-reply .captcha .captcha_html").on("click", function() { actually_load_captcha(provider); });
}); });
}); });
} }

View File

@ -517,12 +517,7 @@ if (isset($_POST['delete'])) {
} }
try { try {
$query = new SecureImageCaptchaQuery( $query = $context->get(SecureImageCaptchaQuery::class);
$context->get(HttpDriver::class),
$config['domain'],
$config['captcha']['provider_check'],
$config['captcha']['extra']
);
$success = $query->verify( $success = $query->verify(
$_POST['captcha_text'], $_POST['captcha_text'],
$_POST['captcha_cookie'] $_POST['captcha_cookie']
@ -632,22 +627,22 @@ if (isset($_POST['delete'])) {
$dynamic = $config['captcha']['dynamic']; $dynamic = $config['captcha']['dynamic'];
// With our custom captcha provider // With our custom captcha provider
if (($provider === 'native' && !$new_thread_capt) if ($provider === 'native') {
|| ($provider === 'native' && $new_thread_capt && $post['op'])) { if ((!$new_thread_capt && !$post['op']) || ($new_thread_capt && $post['op'])) {
$query = $context->get(SecureImageCaptchaQuery::class); $query = $context->get(SecureImageCaptchaQuery::class);
$success = $query->verify($_POST['captcha_text'], $_POST['captcha_cookie']); $success = $query->verify($_POST['captcha_text'], $_POST['captcha_cookie']);
if (!$success) { if (!$success) {
error( error(
"{$config['error']['captcha']} "{$config['error']['captcha']}
<script> <script>
if (actually_load_captcha !== undefined) if (actually_load_captcha !== undefined)
actually_load_captcha( actually_load_captcha(
\"{$config['captcha']['provider_get']}\", \"{$config['captcha']['provider_get']}\"
\"{$config['captcha']['extra']}\" );
); </script>"
</script>" );
); }
} }
} }
// Remote 3rd party captchas. // Remote 3rd party captchas.

View File

@ -1,73 +1,88 @@
<?php <?php
require_once('inc/bootstrap.php'); require_once('inc/bootstrap.php');
$expires_in = 120;
function rand_string($length, $charset) { function rand_string(int $length, string $charset): string {
$ret = ""; $ret = '';
while ($length--) { while ($length--) {
$ret .= mb_substr($charset, rand(0, mb_strlen($charset, 'utf-8')-1), 1, 'utf-8'); $ret .= mb_substr($charset, rand(0, mb_strlen($charset, 'utf-8')-1), 1, 'utf-8');
} }
return $ret; return $ret;
} }
function cleanup() { function cleanup(int $expires_in): void {
global $expires_in;
prepare("DELETE FROM `captchas` WHERE `created_at` < ?")->execute([time() - $expires_in]); prepare("DELETE FROM `captchas` WHERE `created_at` < ?")->execute([time() - $expires_in]);
} }
function handleGetRequestCaptcha(array $config): void {
$extra = $config['captcha']['native']['extra'];
$cookie = rand_string(20, $extra);
$mode = @$_GET['mode']; $securimage = new Securimage($config['captcha']['native']['securimage_options']);
switch ($mode) { $securimage->createCode();
ob_start();
$securimage->show();
$rawImage = ob_get_clean();
$base64Image = 'data:image/png;base64,' . base64_encode($rawImage);
$html = '<img src="' . $base64Image . '">';
$captchaCode = $securimage->getCode();
prepare("INSERT INTO `captchas` (`cookie`, `extra`, `text`, `created_at`) VALUES (?, ?, ?, ?)")
->execute([$cookie, $extra, $captchaCode->code_display, $captchaCode->creationTime]);
if (isset($_GET['raw'])) {
$_SESSION['captcha_cookie'] = $cookie;
header('Content-Type: image/png');
echo $rawImage;
} else {
header("Content-Type: application/json");
echo json_encode([
"cookie" => $cookie,
"captchahtml" => $html,
"expires_in" => $config['captcha']['native']['expires_in'],
]);
}
}
function handleCheckRequestCaptcha(int $expires_in): void {
cleanup($expires_in);
$cookie = $_GET['cookie'] ?? null;
$text = $_GET['text'] ?? null;
if (!$cookie || !$text) {
echo json_encode(["success" => false]);
return;
}
$query = prepare("SELECT * FROM `captchas` WHERE `cookie` = ?");
$query->execute([$cookie]);
$captchaData = $query->fetchAll();
if (!$captchaData) {
echo json_encode(["success" => false]);
return;
}
prepare("DELETE FROM `captchas` WHERE `cookie` = ?")->execute([$cookie]);
$isSuccessful = $captchaData[0]['text'] === $text;
echo json_encode(["success" => $isSuccessful]);
}
$mode = $_GET['mode'] ?? null;
switch($mode) {
case 'get': case 'get':
if (!isset ($_GET['extra'])) { handleGetRequestCaptcha($config);
$_GET['extra'] = $config['captcha']['extra'];
}
header("Content-type: application/json");
$extra = $_GET['extra'];
$cookie = rand_string(20, "abcdefghijklmnopqrstuvwxyz");
$i = new Securimage(['send_headers' => false, 'no_exit' => true]);
$i->createCode();
ob_start();
$i->show();
$rawimg = ob_get_contents();
$b64img = 'data:image/png;base64,'.base64_encode($rawimg);
$html = '<img src="'.$b64img.'">';
ob_end_clean();
$cdata = $i->getCode();
$query = prepare("INSERT INTO `captchas` (`cookie`, `extra`, `text`, `created_at`) VALUES (?, ?, ?, ?)");
$query->execute([$cookie, $extra, $cdata->code_display, $cdata->creationTime]);
if (isset($_GET['raw'])) {
$_SESSION['captcha_cookie'] = $cookie;
header('Content-Type: image/png');
echo $rawimg;
} else {
echo json_encode(["cookie" => $cookie, "captchahtml" => $html, "expires_in" => $expires_in]);
}
break; break;
case 'check': case 'check':
cleanup(); handleCheckRequestCaptcha($config['captcha']['native']['expires_in']);
if (!isset ($_GET['mode']) || !isset ($_GET['cookie']) || !isset ($_GET['extra']) || !isset ($_GET['text'])) {
die();
}
$query = prepare("SELECT * FROM `captchas` WHERE `cookie` = ? AND `extra` = ?");
$query->execute([$_GET['cookie'], $_GET['extra']]);
$ary = $query->fetchAll();
if (!$ary) { // captcha expired
echo "0";
break;
} else {
$query = prepare("DELETE FROM `captchas` WHERE `cookie` = ? AND `extra` = ?");
$query->execute([$_GET['cookie'], $_GET['extra']]);
}
if ($ary[0]['text'] !== $_GET['text']) {
echo "0";
} else {
echo "1";
}
break; break;
} case '':
default:
http_response_code(400);
echo json_encode(["success" => false, "error" => "Invalid mode"]);
break;
}

View File

@ -100,38 +100,22 @@
</td> </td>
</tr> </tr>
{% endif %} {% endif %}
{% if config.captcha.provider == 'native' %} {% if (config.captcha.provider == 'native' and not config.captcha.native.new_thread_capt) or
<tr class='captcha'> (config.captcha.provider == 'native' and config.captcha.native.new_thread_capt and not id) %}
<th> <tr class='captcha'>
{% trans %}Verification{% endtrans %} <th>
</th> {% trans %}Verification{% endtrans %}
<td> </th>
<script>load_captcha("{{ config.captcha.native.provider_get }}", "{{ config.captcha.native.extra }}");</script> <td>
<noscript> <script>load_captcha("{{ config.captcha.native.provider_get }}");</script>
<input class='captcha_text' type='text' name='captcha_text' size='32' maxlength='6' autocomplete='off'> <noscript>
<div class="captcha_html"> <input class='captcha_text' type='text' name='captcha_text' size='32' maxlength='6' autocomplete='off'>
<img src="/{{ config.captcha.native.provider_get }}?mode=get&raw=1"> <div class="captcha_html">
</div> <img src="/{{ config.captcha.native.provider_get }}?mode=get&raw=1">
</noscript> </div>
</td> </noscript>
</tr> </td>
{% elseif config.captcha.native.new_thread_capt %} </tr>
{% if not id %}
<tr class='captcha'>
<th>
{% trans %}Verification{% endtrans %}
</th>
<td>
<script>load_captcha("{{ config.captcha.native.provider_get }}", "{{ config.captcha.native.extra }}");</script>
<noscript>
<input class='captcha_text' type='text' name='captcha_text' size='32' maxlength='6' autocomplete='off'>
<div class="captcha_html">
<img src="/{{ config.captcha.native.provider_get }}?mode=get&raw=1">
</div>
</noscript>
</td>
</tr>
{% endif %}
{% endif %} {% endif %}
{% if config.user_flag %} {% if config.user_flag %}
<tr> <tr>