0

Session security — session hijacking, session fixation, secure cookies

Intermediate5 min read·php-16-007
securityinterview

Concept

PHP's cryptography functions for generating random data, symmetric encryption, and MACs. Using cryptographically secure randomness and authenticated encryption is essential for session tokens, CSRF tokens, API keys, and data confidentiality.

Cryptographically secure random data:

  • random_bytes(int $length): Generates $length cryptographically secure random bytes. Uses OS entropy (CSPRNG): /dev/urandom on Linux, CryptGenRandom on Windows. Safe for tokens, keys, salt. Never use rand(), mt_rand(), or uniqid() for security purposes — they're not cryptographically secure.
  • random_int(int $min, int $max): Cryptographically secure random integer. Use for security-sensitive range selection.

hash_hmac(string $algo, string $data, string $key): Computes an HMAC (Hash-based Message Authentication Code). Use for: API webhook verification (verify Stripe signatures), JWT signatures (HMAC-SHA256), integrity checks on cookies. Never use plain hash() for authentication — it's vulnerable to length-extension attacks; HMAC is not.

openssl_encrypt() / openssl_decrypt(): Symmetric encryption with OpenSSL. Use aes-256-gcm (AEAD — provides both confidentiality and integrity). Must generate a random IV (initialization vector) for each encryption. Store IV alongside ciphertext.

Timing-safe comparison: Use hash_equals(string $known, string $user) to compare secrets. PHP's === operator may be timing-dependent on old hardware/JIT configurations. hash_equals guarantees constant-time comparison — essential for HMAC and token verification.

Code Example

php
<?php
declare(strict_types=1);

// Secure random token generation
$apiKey    = bin2hex(random_bytes(32));    // 64-char hex string
$csrfToken = base64_encode(random_bytes(32)); // URL-safe alternative
$resetToken = bin2hex(random_bytes(20));   // 40-char hex

// Secure random integer (e.g., for security codes)
$otp = random_int(100000, 999999); // 6-digit OTP
$shuffled = array_map(fn() => random_int(1, 100), range(1, 10));

// HMAC — API webhook signature verification (Stripe pattern)
$secret = env('STRIPE_WEBHOOK_SECRET'); // e.g., whsec_xxx
$payload = file_get_contents('php://input');
$signatureHeader = $_SERVER['HTTP_STRIPE_SIGNATURE'] ?? '';

// Parse the header: t=timestamp,v1=signature
preg_match('/t=(\d+),v1=([a-f0-9]+)/', $signatureHeader, $matches);
[$_, $timestamp, $signature] = $matches;

$expectedSignature = hash_hmac('sha256', "$timestamp.$payload", $secret);
if (!hash_equals($expectedSignature, $signature)) { // timing-safe
    http_response_code(400);
    die("Invalid signature");
}

// Symmetric encryption with AES-256-GCM (authenticated encryption)
function encrypt(string $plaintext, string $key32bytes): string
{
    $iv = random_bytes(12); // 12 bytes for GCM
    $ciphertext = openssl_encrypt(
        $plaintext,
        'aes-256-gcm',
        $key32bytes,
        OPENSSL_RAW_DATA,
        $iv,
        $tag,         // authentication tag (16 bytes)
        '',           // additional authenticated data
        16
    );
    // Store: iv + tag + ciphertext
    return base64_encode($iv . $tag . $ciphertext);
}

function decrypt(string $encoded, string $key32bytes): string
{
    $raw  = base64_decode($encoded);
    $iv   = substr($raw, 0, 12);
    $tag  = substr($raw, 12, 16);
    $data = substr($raw, 28);

    $plaintext = openssl_decrypt($data, 'aes-256-gcm', $key32bytes, OPENSSL_RAW_DATA, $iv, $tag);
    if ($plaintext === false) {
        throw new \RuntimeException("Decryption failed — data may be tampered");
    }
    return $plaintext;
}

$key = random_bytes(32); // 256-bit key
$enc = encrypt("sensitive data", $key);
$dec = decrypt($enc, $key); // "sensitive data"

// Laravel uses its own Crypt facade (AES-256-CBC + HMAC under the hood)
// Crypt::encryptString($value) / Crypt::decryptString($encrypted)