Session security — session hijacking, session fixation, secure cookies
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$lengthcryptographically secure random bytes. Uses OS entropy (CSPRNG):/dev/urandomon Linux,CryptGenRandomon Windows. Safe for tokens, keys, salt. Never userand(),mt_rand(), oruniqid()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
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)