24) { return false; } if (strlen($_POST['t-challenge']) > 255) { return false; } // User agent if (!isset($_SERVER['HTTP_USER_AGENT'])) { $user_agent = '!'; } else { $user_agent = md5($_SERVER['HTTP_USER_AGENT']); } // Password $password = '!'; if ($userpwd && !$userpwd->isNew()) { $password = $userpwd->getPwd(); } // --- list($uniq_id, $challenge_hash) = explode('.', $_POST['t-challenge']); $long_ip = ip2long($ip); if (!$uniq_id || !$challenge_hash || !$long_ip) { return false; } $challenge_key = "ch$long_ip"; $params = [$ip, $password, $user_agent, $board, $thread_id]; $response = TwisterCaptcha::normalizeReponseStr($_POST['t-response']); $is_valid = TwisterCaptcha::verifyChallengeHash($challenge_hash, $uniq_id, $response, $params); if ($is_valid) { $active_uniq_id = $memcached->get($challenge_key); // Delete challenge $memcached->delete($challenge_key); if (!$active_uniq_id || $uniq_id !== $active_uniq_id) { return false; } // Return and decrement the unsolved session count $us = decrement_twister_captcha_session($memcached, $ip, $unsolved_count !== null); if ($unsolved_count !== null) { $unsolved_count = $us; } return true; } // Delete challenge $memcached->delete($challenge_key); return false; } // Decrements the unsolved count by 2 and returns the old count function decrement_twister_captcha_session($memcached, $ip, $return_old = true) { if (!$memcached) { return false; } $long_ip = ip2long($ip); if (!$long_ip) { return false; } $key = "us$long_ip"; if ($return_old) { $val = $memcached->get($key); if ($val === false) { $val = 0; } } else { $val = 0; } $memcached->decrement($key, 2); return $val; } // FIXME: The IP arg isn't used for now function set_twister_captcha_credits($memcached, $ip, $userpwd, $current_time) { if (!$memcached || !$userpwd) { return false; } $current_time = (int)$current_time; if ($current_time <= 0) { return false; } //$long_ip = ip2long($ip); //if (!$long_ip) { //return false; //} $credits = 0; // Config // Stage 1 should match the check in use_twister_captcha_credit() // and captcha.php for optimisation purposes // Stage 1 $noop_known_ttl_1 = 4320; // required user lifetime (3 days, in minutes) $noop_post_count_1 = 5; // required post count $noop_credits_1 = 1; // credits given $noop_duration_1 = 3600; // duration of the credits (1 hour, in seconds) // Stage 2 $noop_known_ttl_2 = 21600; // 15 days $noop_post_count_2 = 20; $noop_credits_2 = 2; $noop_duration_2 = 7200; // 2 hours // Stage 3 $noop_known_ttl_3 = 129600; // 90 days $noop_post_count_3 = 100; $noop_credits_3 = 3; $noop_duration_3 = 10800; // 3 hours // --- // The IP changed too recently if ($userpwd->ipLifetime() < 60) { return false; } // Stage 3 if ($userpwd->isUserKnown($noop_known_ttl_3) && $userpwd->postCount() >= $noop_post_count_3) { $credits = $noop_credits_3; $duration = $noop_duration_3; } // Stage 2 else if ($userpwd->isUserKnown($noop_known_ttl_2) && $userpwd->postCount() >= $noop_post_count_2) { $credits = $noop_credits_2; $duration = $noop_duration_2; } // Stage 1 else if ($userpwd->isUserKnown($noop_known_ttl_1) && $userpwd->postCount() >= $noop_post_count_1) { $credits = $noop_credits_1; $duration = $noop_duration_1; } else { return false; } if (!$credits || $credits > 3) { return false; } $expiration_ts = $current_time + $duration; // Require no more than 5 actions in the past 8 minutes /* $query = <<= DATE_SUB(NOW(), INTERVAL 8 MINUTE) SQL; $res = mysql_global_call($query); if (!$res) { return false; } $count = (int)mysql_fetch_row($res)[0]; if ($count > 5) { return false; } */ // Set credits $pwd = $userpwd->getPwd(); if (!$pwd) { return false; } $key = "cr-$pwd"; $val = "$credits.$expiration_ts"; $res = $memcached->replace($key, $val, $expiration_ts); if ($res === false) { if ($memcached->getResultCode() === Memcached::RES_NOTSTORED) { return $memcached->set($key, $val, $expiration_ts); } else { return false; } } return true; } // FIXME: The IP arg isn't used for now function use_twister_captcha_credit($memcached, $ip, $userpwd) { if (!$memcached || !$userpwd) { return false; } //$long_ip = ip2long($ip); //if (!$long_ip) { //return false; //} // Must match the check in set_twister_captcha_credits() $noop_known_ttl_1 = 4320; // required user lifetime (3 days, in minutes) $noop_post_count_1 = 5; // required post count if (!$userpwd->isUserKnown($noop_known_ttl_1) || $userpwd->postCount() < $noop_post_count_1) { return false; } $pwd = $userpwd->getPwd(); if (!$pwd) { return false; } $key = "cr-$pwd"; $credits = $memcached->get($key); if ($credits === false) { return false; } list($count, $ts) = explode('.', $credits); $count = (int)$count; $ts = (int)$ts; // No credits left if ($count <= 0 || $ts <= 0) { $memcached->delete($key); return false; } $count -= 1; $res = $memcached->replace($key, "$count.$ts", $ts); if ($res === false && $memcached->getResultCode() !== Memcached::RES_NOTSTORED) { return false; } return true; } function twister_captcha_form() { return '
'; } function log_failed_captcha($ip, $userpwd, $board, $thread_id, $is_quiet, $meta = null) { $data = [ 'board' => $board, 'thread_id' => $thread_id, ]; if ($userpwd) { $data['arg_num'] = $userpwd->pwdLifetime(); $data['pwd'] = $userpwd->getPwd(); } if ($meta) { $data['meta'] = $meta; } if ($is_quiet) { $type = 'failed_captcha_quiet'; } else { $type = 'failed_captcha'; } write_to_event_log($type, $ip, $data); } function h_captcha_form($autoload = false, $cb = 'onRecaptchaLoaded', $dark = false) { global $hcaptcha_public_key; $js_tag = ''; if ($autoload) { $attrs = ' class="h-captcha" data-sitekey="' . $hcaptcha_public_key . '"'; if ($dark) { $attrs .= ' data-theme="dark"'; } } else { $attrs = ''; } $container_tag = '
'; return $js_tag.$container_tag; } // Moves css out of the form for html validation function captcha_form($autoload = false, $cb = 'onRecaptchaLoaded', $dark = false) { global $recaptcha_public_key; $js_tag = ''; if ($autoload) { $attrs = ' class="g-recaptcha" data-sitekey="' . $recaptcha_public_key . '"'; if ($dark) { $attrs .= ' data-theme="dark"'; } } else { $attrs = ''; } $container_tag = '
'; $noscript_tag =<<
HTML; if (defined('NOSCRIPT_CAPTCHA_ONLY') && NOSCRIPT_CAPTCHA_ONLY == 1) { return $container_tag.$noscript_tag; } return $js_tag.$container_tag.$noscript_tag; } // Legacy captcha // Uses recaptcha v2 for noscript captcha as the v1 seems to be broken currently. function captcha_form_alt() { global $recaptcha_public_key; $html = << HTML; return $html; } function recaptcha_ban($n, $time, $return_error = 0, $length = 1) { auto_ban_poster($name, $length, 1, "failed verification $n times per $time", "Possible spambot; repeatedly sent incorrect CAPTCHA verification."); if( $return_error == 1 ) { return S_GENERICERROR; } error(S_GENERICERROR); } /** * Works for both recaptcha and hcaptcha */ function recaptcha_bad_captcha($return_error = false, $codes = null) { $error = S_BADCAPTCHA; if (is_array($codes)) { if (in_array('missing-input-response', $codes)) { $error = S_NOCAPTCHA; } if ($return_error) { return $error; } else { error($error); } } else { if ($return_error) { return $error; } else { error($error); } } } // ----------- // hCaptcha // ----------- function start_hcaptcha_verify($return_error = false) { global $hcaptcha_private_key, $hcaptcha_ch; $response = $_POST["h-captcha-response"]; if (!$response) { if ($return_error == false) { error(S_NOCAPTCHA); } return S_NOCAPTCHA; } $response = urlencode($response); $rlen = strlen($response); if ($rlen > 32768) { return recaptcha_bad_captcha($return_error); } $api_url = 'https://hcaptcha.com/siteverify'; $post = array( 'secret' => $hcaptcha_private_key, 'response' => $response ); $hcaptcha_ch = rpc_start_captcha_request($api_url, $post, null, false); } function end_hcaptcha_verify($return_error = false) { global $hcaptcha_ch; if (!$hcaptcha_ch) { return; } $ret = rpc_finish_request($hcaptcha_ch, $error, $httperror); // BAD // 413 Request Too Large is bad; it was caused intentionally by the user. if ($httperror == 413) { return recaptcha_bad_captcha($return_error); } // BAD if ($ret == null) { return recaptcha_bad_captcha($return_error); } $resp = json_decode($ret, true); // BAD // Malformed JSON response from Google if (json_last_error() !== JSON_ERROR_NONE) { return recaptcha_bad_captcha($return_error); } // GOOD if ($resp['success']) { return $resp; } // BAD return recaptcha_bad_captcha($return_error, $resp['error-codes']); } // ----------- // reCaptcha V2 // ----------- // FIXME $challenge_field is no longer used function start_recaptcha_verify($return_error = false, $challenge_field = '') { global $recaptcha_private_key, $recaptcha_ch; $response = $_POST["g-recaptcha-response"]; if (!$response) { if ($return_error == false) { error(S_NOCAPTCHA); } return S_NOCAPTCHA; } $response = urlencode($response); $rlen = strlen($response); if ($rlen > 4096) { return recaptcha_bad_captcha($return_error); } $api_url = 'https://www.google.com/recaptcha/api/siteverify'; $post = array( 'secret' => $recaptcha_private_key, 'response' => $response ); $recaptcha_ch = rpc_start_captcha_request($api_url, $post, null, false); } function end_recaptcha_verify($return_error = false) { global $recaptcha_ch; if (!$recaptcha_ch) { return; } $ret = rpc_finish_request($recaptcha_ch, $error, $httperror); // BAD // 413 Request Too Large is bad; it was caused intentionally by the user. if ($httperror == 413) { return recaptcha_bad_captcha($return_error); } // BAD if ($ret == null) { return recaptcha_bad_captcha($return_error); } $resp = json_decode($ret, true); // BAD // Malformed JSON response from Google if (json_last_error() !== JSON_ERROR_NONE) { return recaptcha_bad_captcha($return_error); } // GOOD if ($resp['success']) { return $resp; } // BAD return recaptcha_bad_captcha($return_error, $resp['error-codes']); } // ----------- // reCaptcha V1 // ----------- function start_recaptcha_verify_alt($return_error = false, $challenge_field = '') { global $recaptcha_private_key, $recaptcha_ch; $challenge = ( $challenge_field == '' ) ? $_POST["recaptcha_challenge_field"] : $challenge_field; $response = $_POST["recaptcha_response_field"]; if (!$challenge || !$response) { if( $return_error == false ) { error(S_NOCAPTCHA); } return S_NOCAPTCHA; } $num_words = 1 + preg_match_all('/\\s/', $response); $rlen = strlen($response); if ($num_words > 3 || $rlen > 128) { return recaptcha_bad_captcha($return_error); } $post = array( "privatekey" => $recaptcha_private_key, "challenge" => $challenge, "remoteip" => $_SERVER["REMOTE_ADDR"], "response" => $response ); $recaptcha_ch = rpc_start_request("https://www.google.com/recaptcha/api/verify", $post, null, false); } function end_recaptcha_verify_alt($return_error = false) { global $recaptcha_ch; if (!$recaptcha_ch) return; $ret = rpc_finish_request($recaptcha_ch, $error, $httperror); if ($httperror == 413) { return recaptcha_bad_captcha($return_error); } if ($ret) { $lines = explode("\n", $ret); if ($lines[0] === "true") { // GOOD return; } } // BAD return recaptcha_bad_captcha($return_error); } ?>