1

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:

  1. Works for both logged-in and non-logged-in users

  2. Doesn't require user authentication

  3. Uses session cookies or browser fingerprints

My Question:

How can I generate and track unique session identifiers securely?

1
  • Just starting the session in PHP pretty much does it. The ID is unique and all normally behaving browsers keep track of it on their own. You just then store information within the $_SESSION to keep track of their recent queries Commented Nov 24 at 9:41

1 Answer 1

3

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)

  • Add this helper method to your class (creates a secure random id, sets a cookie with HttpOnly/Secure/SameSite when possible, and returns a session identifier string or empty on failure):
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;
}
  • Replace the start of check_daily_limit with using the session identifier (minimal edits; rest of your routine is unchanged). Replace your existing check_daily_limit($ip) with this version:
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;
}
Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.