ARGUS WordPress Defence 1.0.0 — first production release
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.
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
<?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 );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user