I’ve written a custom script that generates a fingerprint based on both server-side and client-side data to block users who keep coming back with different IP addresses. I know it’s not 100% foolproof, but it’s a last-resort failsafe that is currently working smoothly.
The issue I’m encountering occurs when a page is first loaded in a new session. The PHP code is executed before the JavaScript code has a chance to collect the client-side data. Specifically, the fingerprint function is called, but since there is no fingerprint in the session at that point, generate_fingerprint() is triggered. However, when the client-side data is collected, there is no POST data available (because the JavaScript hasn’t run yet), causing the function to return null. This causes the blocking mechanism to fail in this scenario.
This is the code piece I currently have (in combination with a javascript file) to generate the client-data based information for the fingerprint:
function generate_advanced_fingerprint() {
$components = [
'screen_resolution' => $_POST['screen_resolution'] ?? '',
'color_depth' => $_POST['color_depth'] ?? '',
'canvas_fingerprint' => $_POST['canvas_fingerprint'] ?? '',
'webgl_fingerprint' => $_POST['webgl_fingerprint'] ?? '',
'audio_fingerprint' => $_POST['audio_fingerprint'] ?? '',
'installed_fonts' => $_POST['installed_fonts'] ?? '',
'hardware_concurrency' => $_POST['hardware_concurrency'] ?? '',
'device_memory' => $_POST['device_memory'] ?? '',
'browser_features' => $_POST['browser_features'] ?? ''
];
if (in_array('', $components, true)) {
return null;
}
ksort($components);
$structured_data = json_encode($components);
$fingerprint = md5($structured_data);
return $fingerprint;
}
Note: The javascript is execute in the top of the <head> section
This is the code I use to generate the fingerprint and do the block check against the blocklist (a JSON file):
function handle_fingerprint() {
if (!is_page('test')) {
return;
}
$is_post_request = ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_SERVER['HTTP_X_FINGERPRINT_REQUEST']));
if ($is_post_request) {
// Process the POST data with client-side information
$raw_post_data = file_get_contents('php://input');
$json_data = json_decode($raw_post_data, true);
if ($json_data) {
$_POST = array_merge($_POST, $json_data);
}
}
// Generate or retrieve the fingerprint
if (!isset($_SESSION['fingerprint_generated']) || $is_post_request) {
$fingerprint = generate_fingerprint();
$_SESSION['fingerprint_generated'] = true;
$_SESSION['current_fingerprint'] = $fingerprint;
// Save the fingerprint if it is a POST request
if ($is_post_request) {
$ip = get_visitor_ip();
save_fingerprint($ip, $fingerprint);
// Send a response to the POST request
header('Content-Type: application/json');
echo json_encode(['success' => true]);
exit;
}
} else {
$fingerprint = $_SESSION['current_fingerprint'];
}
// Run the check
$blocked_fingerprints = get_blocked_fingerprints();
// Check fallback fingerprint
if (in_array($fingerprint['fallback_fingerprint'], $blocked_fingerprints)) {
wp_safe_redirect(home_url('/'));
exit;
}
// Check advanced fingerprint
if (isset($fingerprint['advanced_fingerprint']) && in_array($fingerprint['advanced_fingerprint'], $blocked_fingerprints)) {
wp_safe_redirect(home_url('/'));
exit;
}
}
// Check the fingerprint on every pageload
add_action('wp', 'handle_fingerprint');
// Process POST requests for fingerprint data
add_action('init', function() {
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_SERVER['HTTP_X_FINGERPRINT_REQUEST'])) {
handle_fingerprint();
}
});
In this scenario (with a new session), after the page loads, the JavaScript executes window.getFingerprint(). This function collects client-side data and sends it to the server via a POST request. This triggers the handle_fingerprint() function again. Now the POST data is available, so generate_advanced_fingerprint() can execute successfully. A complete fingerprint is generated.
The result is a double call (which I can make visible via my error logs).
I’ve tried generating the fingerprint only once all the data is available:
$is_post_request = ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_SERVER['HTTP_X_FINGERPRINT_REQUEST']));
if ($is_post_request) {
// Process the POST data with client-side information
$raw_post_data = file_get_contents('php://input');
$json_data = json_decode($raw_post_data, true);
if ($json_data) {
$_POST = array_merge($_POST, $json_data);
}
}
Which works, but by the time the clientdata is available, the content has already been served, and the redirect mechanism doesn’t work anymore. Now, I’m trying to find a way to delay the blocking check until the client-side data is available, but so far, I haven’t been successful.
Note:
-
I want to avoid showing error messages. I rather have them redirected to a “faux” page designed to distract rather than making them any wiser than they are.
-
I also want to avoid using javascript for the redirect. I would like to stay with the wp_safe_redirect() or something likewise if possible.
Can anyone help me out with a fresh look at it?