I'm developing a WordPress plugin that provides chat functionality via REST API endpoints. Currently, I'm using IP-based rate limiting, but this causes issues in university/company environments where multiple users share the same public IP address behind a router.
Current IP-based rate limiting code:
private function get_client_ip() {
$ip_keys = [
'HTTP_X_REAL_IP',
'HTTP_X_FORWARDED_FOR',
'HTTP_CLIENT_IP',
'REMOTE_ADDR'
];
foreach ($ip_keys as $key) {
if (!empty($_SERVER[$key])) {
$ip = $_SERVER[$key];
if (strpos($ip, ',') !== false) {
$ips = explode(',', $ip);
$ip = trim($ips[0]);
}
if (filter_var($ip, FILTER_VALIDATE_IP)) {
return $ip;
}
}
}
return '0.0.0.0';
}
private function check_daily_limit($ip) {
$daily_limit = $this->rate_limits['daily'];
$limit_key = 'chat2find_daily_limit_' . md5($ip);
$data = get_transient($limit_key);
if ($data === false) {
$data = [
'count' => 1,
'first_request' => time(),
'ip' => $ip,
'endpoint' => 'daily'
];
set_transient($limit_key, $data, $daily_limit['seconds']);
} else {
if ($data['count'] >= $daily_limit['requests']) {
$wait_time = $daily_limit['seconds'] - (time() - $data['first_request']);
return new WP_Error('daily_rate_limit_exceeded',
sprintf('Daily API limit exceeded. Please try again in %d hours.', ceil($wait_time / 3600)),
['status' => 429]
);
}
$data['count']++;
set_transient($limit_key, $data, $daily_limit['seconds']);
}
return true;
}
The Problem:
In environments like universities or companies, multiple users share the same public IP, so legitimate users get blocked when someone else from the same network exceeds the rate limit.
What I Need:
I want to implement session-based rate limiting that:
Works for both logged-in and non-logged-in users
Doesn't require user authentication
Uses session cookies or browser fingerprints
My Question:
How can I generate and track unique session identifiers securely?
I have added a small helper here to create/return a stable, server-generated session id stored in a cookie and I changed check_daily_limit to use that session id as the rate-limit key when available (falling back to IP). I kept the rest of your transient logic intact so the change is minimal and safe to drop into your plugin.
Updated code (only the required parts)
private function get_session_identifier() {
$cookie_name = 'chat2find_sid';
// If cookie exists and looks valid, return it
if (!empty($_COOKIE[$cookie_name])) {
$sid = $_COOKIE[$cookie_name];
// validate expected hex format (32 or 64 hex chars)
if (preg_match('/^[0-9a-f]{32,64}$/', $sid)) {
return 'session_' . $sid;
}
// invalid cookie: fall through to re-create
}
// Create a new session id (32 hex chars = 16 bytes)
try {
$sid = bin2hex(random_bytes(16));
} catch (Exception $e) {
// random_bytes failed (very unlikely) -> no session id
return '';
}
// Cookie lifetime: adjust as needed (example: 30 days)
$lifetime = 30 * DAY_IN_SECONDS;
$expire = time() + $lifetime;
// Set cookie with appropriate flags (PHP 7.3+ supports options array)
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
if (PHP_VERSION_ID >= 70300) {
setcookie($cookie_name, $sid, [
'expires' => $expire,
'path' => '/',
'secure' => $secure,
'httponly' => true,
'samesite' => 'Lax',
]);
} else {
// Best-effort fallback for older PHP: craft header
$cookie = rawurlencode($cookie_name) . '=' . rawurlencode($sid)
. '; Expires=' . gmdate('D, d M Y H:i:s T', $expire)
. '; Path=/'
. ($secure ? '; Secure' : '')
. '; HttpOnly; SameSite=Lax';
header('Set-Cookie: ' . $cookie, false);
}
return 'session_' . $sid;
}
private function check_daily_limit($ip) {
$daily_limit = $this->rate_limits['daily'];
// Prefer a stable session identifier for anonymous users (avoids shared-IP blocking).
// Falls back to IP when session id cannot be created (very rare).
$identifier = $this->get_session_identifier();
if (empty($identifier)) {
$identifier = 'ip_' . $ip;
}
$limit_key = 'chat2find_daily_limit_' . md5($identifier);
$data = get_transient($limit_key);
if ($data === false) {
$data = [
'count' => 1,
'first_request' => time(),
'identifier' => $identifier,
'endpoint' => 'daily'
];
set_transient($limit_key, $data, $daily_limit['seconds']);
} else {
if ($data['count'] >= $daily_limit['requests']) {
$wait_time = $daily_limit['seconds'] - (time() - $data['first_request']);
return new WP_Error('daily_rate_limit_exceeded',
sprintf('Daily API limit exceeded. Please try again in %d hours.', ceil($wait_time / 3600)),
['status' => 429]
);
}
$data['count']++;
set_transient($limit_key, $data, $daily_limit['seconds']);
}
return true;
}