Automatic WordPress security: local firewall, malware and file-integrity scanning, vulnerability protection, quarantine, scheduled backups, an optional page cache, and automatic global threat intelligence. See README.md for installation, update, and uninstall instructions.
147 lines
4.5 KiB
PHP
147 lines
4.5 KiB
PHP
<?php
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
class Argus_Challenge {
|
|
|
|
const TRANSIENT_PREFIX = 'argus_wpd_pow_';
|
|
const COOKIE_NAME = 'argus_wpd_ct';
|
|
const DEFAULT_DIFFICULTY = 4;
|
|
const TTL_SECONDS = 120;
|
|
const PASS_TTL_SECONDS = 30 * MINUTE_IN_SECONDS;
|
|
|
|
public static function issue( $ip ) {
|
|
$challenge_id = wp_generate_password( 20, false );
|
|
$prefix = wp_generate_password( 16, false );
|
|
$difficulty = (int) apply_filters( 'argus_wpd_pow_difficulty', self::DEFAULT_DIFFICULTY );
|
|
|
|
set_transient(
|
|
self::TRANSIENT_PREFIX . $challenge_id,
|
|
array(
|
|
'ip' => $ip,
|
|
'prefix' => $prefix,
|
|
'difficulty' => $difficulty,
|
|
'issued_at' => time(),
|
|
),
|
|
self::TTL_SECONDS
|
|
);
|
|
|
|
return array(
|
|
'challenge_id' => $challenge_id,
|
|
'prefix' => $prefix,
|
|
'difficulty' => $difficulty,
|
|
);
|
|
}
|
|
|
|
public static function verify( $challenge_id, $counter, $ip ) {
|
|
$data = get_transient( self::TRANSIENT_PREFIX . $challenge_id );
|
|
if ( ! $data || $data['ip'] !== $ip ) {
|
|
return false;
|
|
}
|
|
|
|
$hash = hash( 'sha256', $data['prefix'] . $counter );
|
|
$ok = 0 === strncmp( $hash, str_repeat( '0', $data['difficulty'] ), $data['difficulty'] );
|
|
|
|
if ( $ok ) {
|
|
|
|
delete_transient( self::TRANSIENT_PREFIX . $challenge_id );
|
|
self::issue_pass_cookie( $ip );
|
|
}
|
|
|
|
return $ok;
|
|
}
|
|
|
|
protected static function issue_pass_cookie( $ip ) {
|
|
$token = wp_generate_password( 32, false );
|
|
set_transient( 'argus_wpd_pass_' . $token, $ip, self::PASS_TTL_SECONDS );
|
|
setcookie( self::COOKIE_NAME, $token, time() + self::PASS_TTL_SECONDS, COOKIEPATH ?: '/', COOKIE_DOMAIN, is_ssl(), true );
|
|
}
|
|
|
|
public static function has_valid_pass( $ip ) {
|
|
if ( empty( $_COOKIE[ self::COOKIE_NAME ] ) ) { // phpcs:ignore
|
|
return false;
|
|
}
|
|
$token = sanitize_text_field( wp_unslash( $_COOKIE[ self::COOKIE_NAME ] ) ); // phpcs:ignore
|
|
$stored_ip = get_transient( 'argus_wpd_pass_' . $token );
|
|
return $stored_ip && hash_equals( (string) $stored_ip, $ip );
|
|
}
|
|
|
|
public static function render_and_exit( $ip ) {
|
|
$challenge = self::issue( $ip );
|
|
nocache_headers();
|
|
status_header( 429 );
|
|
header( 'Content-Type: text/html; charset=utf-8' );
|
|
?>
|
|
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Verifying your browser…</title>
|
|
<meta name="robots" content="noindex">
|
|
<style>
|
|
body{font-family:system-ui,sans-serif;background:#0b0f19;color:#e2e8f0;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}
|
|
.box{text-align:center;max-width:420px;padding:32px}
|
|
.spinner{width:32px;height:32px;border:3px solid rgba(148,163,184,.25);border-top-color:#00d4ff;border-radius:50%;margin:0 auto 20px;animation:spin 1s linear infinite}
|
|
@keyframes spin{to{transform:rotate(360deg)}}
|
|
p{color:#94a3b8;font-size:13px}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="box">
|
|
<div class="spinner"></div>
|
|
<h3>Verifying your browser</h3>
|
|
<p>This site is protected by ARGUS WordPress Defence. This should only take a moment.</p>
|
|
</div>
|
|
<script>
|
|
(async function () {
|
|
const prefix = <?php echo wp_json_encode( $challenge['prefix'] ); ?>;
|
|
const difficulty = <?php echo (int) $challenge['difficulty']; ?>;
|
|
const challengeId = <?php echo wp_json_encode( $challenge['challenge_id'] ); ?>;
|
|
const target = '0'.repeat(difficulty);
|
|
|
|
async function sha256Hex(input) {
|
|
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input));
|
|
return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
}
|
|
|
|
let counter = 0;
|
|
while (true) {
|
|
const hash = await sha256Hex(prefix + counter);
|
|
if (hash.startsWith(target)) break;
|
|
counter++;
|
|
}
|
|
|
|
const form = document.createElement('form');
|
|
form.method = 'POST';
|
|
form.action = window.location.href;
|
|
for (const [name, value] of Object.entries({ argus_wpd_challenge_id: challengeId, argus_wpd_counter: counter })) {
|
|
const input = document.createElement('input');
|
|
input.type = 'hidden';
|
|
input.name = name;
|
|
input.value = value;
|
|
form.appendChild(input);
|
|
}
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|
|
<?php
|
|
exit;
|
|
}
|
|
|
|
public static function maybe_handle_submission( $ip ) {
|
|
if ( empty( $_POST['argus_wpd_challenge_id'] ) || ! isset( $_POST['argus_wpd_counter'] ) ) { // phpcs:ignore
|
|
return false;
|
|
}
|
|
|
|
$challenge_id = sanitize_text_field( wp_unslash( $_POST['argus_wpd_challenge_id'] ) ); // phpcs:ignore
|
|
$counter = sanitize_text_field( wp_unslash( $_POST['argus_wpd_counter'] ) ); // phpcs:ignore
|
|
|
|
return self::verify( $challenge_id, $counter, $ip );
|
|
}
|
|
}
|