I'm working on a WordPress site with BuddyPress, the BuddyX Pro theme, and the Wise Chat Pro plugin. Recently, I’ve been tackling abuse on the group chat page, where users often bypass IP-based bans using TOR or VPNs. To improve security, I wrote a custom plugin that does the following:
Plugin functionality:
How it works:
The issue:
I generate a set of 2 fingerprints (advanced and fallback):
function generate_fingerprint() {
$fallback = generate_fallback_fingerprint();
$advanced = generate_advanced_fingerprint();
$combined = md5($fallback . $advanced);
$reliability = calculate_reliability_score();
return [
'fallback_fingerprint' => $fallback,
'advanced_fingerprint' => $combined,
'reliability_score' => $reliability
];
}
This set of fingerprings is than saved to a fingerprints.json logfile:
function save_fingerprint($ip, $fingerprint_data) {
$data = [];
if (file_exists(FINGERPRINT_FILE)) {
$data = json_decode(file_get_contents(FINGERPRINT_FILE), true) ?? [];
}
$timestamp = time();
$key = $ip . '_' . $timestamp;
$data[$key] = [
'ip' => $ip,
'fallback_fingerprint' => $fingerprint_data['fallback_fingerprint'],
'advanced_fingerprint' => $fingerprint_data['advanced_fingerprint'],
'reliability_score' => $fingerprint_data['reliability_score'],
'timestamp' => $timestamp,
// 'client_data' => $_POST // Only enabled when needed for debugging
];
file_put_contents(FINGERPRINT_FILE, json_encode($data, JSON_PRETTY_PRINT));
}
When the user returns, I would expect that the same fingerprint (advanced or fallback) that is generated and logged to the fingerprints.json would be recognized and if held against the blocklist (blocked_fingerprints.json), would result in the user being redirected as expected (in case of a blocked fingerprint).
$blocked_fingerprints = get_blocked_fingerprints();
if (in_array($fingerprint_data['fallback_fingerprint'], $blocked_fingerprints)) {
wp_safe_redirect($redirect_url);
exit;
}
if (in_array($fingerprint_data['advanced_fingerprint'], $blocked_fingerprints)) {
wp_safe_redirect($redirect_url);
exit;
}
However, this does not happen.
The fingerprints generated and checked against the blocklist do not match the one written to the JSON file.
I have tried to see what fingerprints are checked against the blocklist, by adding them to my errorlog:
$fingerprint_data = generate_fingerprint();
save_fingerprint($fingerprint_data);
error_log("Fingerprint saved for IP: $ip");
The error log than shows a different set of fingerprints than the one written to the fingerprints.json file. This suggests there's some kind of mismatch in how the fingerprint is generated upon checking the blocklist. Even for the serverside (fallback) fingerprint, which should always be identical. So there seems to be a different way the fingerprints get calculated and/or hashed and I can not figure out how or why. I have no idea what the difference is between both situations.
My full code files (for review) are located here:
https://pastebin.com/u/flower88/1/JUEMKr6z
The password to the files is: G3F80fhczm
My setup:
What works:
What i’ve tried:
Thank you very much in advance!
I managed to figure this one out by myself eventually.
For reference, this is what I did:
function generate_fingerprint() {
static $cached_fingerprint = null;
if ($cached_fingerprint !== null) {
return $cached_fingerprint;
}
$fallback = generate_fallback_fingerprint();
$advanced_client = generate_advanced_fingerprint();
$combined = md5($fallback . $advanced_client);
$reliability = calculate_reliability_score();
$fingerprint = [
'fallback_fingerprint' => $fallback,
'advanced_fingerprint' => $combined,
'advanced_client_only' => $advanced_client,
'reliability_score' => $reliability
];
$cached_fingerprint = $fingerprint;
return $fingerprint;
}
function save_fingerprint($ip, $fingerprint_data) {
$data = [];
if (file_exists(FINGERPRINT_FILE)) {
$data = json_decode(file_get_contents(FINGERPRINT_FILE), true) ?? [];
}
$timestamp = time();
$key = $ip . '_' . $timestamp;
$data[$key] = [
'ip' => $ip,
'fallback_fingerprint' => $fingerprint_data['fallback_fingerprint'],
'advanced_fingerprint' => $fingerprint_data['advanced_fingerprint'],
'advanced_client_only' => $fingerprint_data['advanced_client_only'],
'reliability_score' => $fingerprint_data['reliability_score'],
'timestamp' => $timestamp,
];
// Voeg client_data toe als de constante is gedefinieerd en true is
if (defined('SAVE_CLIENT_DATA') && SAVE_CLIENT_DATA) {
$data[$key]['client_data'] = $_POST;
}
file_put_contents(FINGERPRINT_FILE, json_encode($data, JSON_PRETTY_PRINT));
}
function is_fingerprint_blocked($fingerprint) {
$blocked_fingerprints = get_blocked_fingerprints();
$is_post_request = ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_SERVER['HTTP_X_FINGERPRINT_REQUEST']));
$redirect_type = $is_post_request ? "JavaScript redirect" : "wp_safe_redirect";
$ip = get_visitor_ip();
$fingerprint_types = [
'fallback_fingerprint' => 'fallback',
'advanced_fingerprint' => 'gecombineerde advanced',
'advanced_client_only' => 'clientside-only advanced'
];
foreach ($fingerprint_types as $key => $type) {
if (isset($fingerprint[$key]) && in_array($fingerprint[$key], $blocked_fingerprints)) {
return true;
}
}
return false;
}
I no longer need help, so resolving my own request.