Back to Blog
SecurityJuly 2, 20264 min read

Timing Attacks: How to Stop Side-Channel Security Leaks

Timing attacks can leak your secrets by measuring execution speed. Learn how to implement constant-time comparison in Node.js and PHP to secure your apps.

securitycryptographynodejsphpbackendperformanceWeb

While debugging a latency issue in a production authentication service, I realized our password verification logic was leaking information through the back door. The server responded about 15ms faster when the first character of a provided API key was incorrect compared to when it was right. That tiny delay is all an attacker needs to brute-force a string character by character.

Understanding Timing Attacks and Side-Channel Security

Timing attacks are a specific class of side-channel security threats where an adversary infers secret information—like private keys, session tokens, or password hashes—by measuring how long a system takes to perform a cryptographic operation. When you use a standard string comparison operator like == in PHP or === in JavaScript, the engine stops as soon as it finds the first mismatched character. This "short-circuiting" behavior is a performance optimization, but in security-sensitive contexts, it’s a vulnerability.

If your code compares a user-provided hash against a stored value, the execution time becomes a function of how many leading characters match. An attacker can send thousands of requests, measure the response times with high precision, and statistically determine the correct characters one by one. It’s not theoretical; it’s a measurable, exploitable data leak.

Implementing Constant-Time Comparison

The fix is straightforward: you must use a constant-time comparison function. Instead of exiting early, these functions compare every byte of the two strings, regardless of whether they match or not, ensuring the total execution time remains identical for any input.

Protecting Node.js Applications

In Node.js, the crypto module provides a native, battle-tested utility for this. Never manually loop through strings to compare them.

JAVASCRIPT
const crypto = require(CE9178">'crypto');

// The wrong way:
// if (userInput === storedSecret) { ... }

// The right way:
const isMatch = crypto.timingSafeEqual(
  Buffer.from(userInput, CE9178">'utf8'),
  Buffer.from(storedSecret, CE9178">'utf8')
);

One caveat: crypto.timingSafeEqual expects buffers of the same length. If you pass buffers of different lengths, it will throw an error. To prevent leaking the length of your secret, you should first hash both the input and the secret using a constant-time HMAC or a similar approach before comparing them, or pad the input to a fixed length.

Protecting PHP Applications

PHP provides a similar utility via the hash_equals function, introduced in version 5.6. It is designed specifically to mitigate these risks.

PHP
#6A9955">// The wrong way:
#6A9955">// if ($userInput == $storedSecret) { ... }

#6A9955">// The right way:
if (hash_equals($storedSecret, $userInput)) {
    #6A9955">// Proceed with authentication
}

This function is essentially the gold standard for PHP developers. It’s simple, effective, and handles the underlying byte-level comparison without short-circuiting.

Comparing Security Approaches

When building robust authentication, you have to choose the right tool for the job. Here is how standard equality checks compare to secure alternatives.

MethodExecution TimeUse Case
Standard == / ===Variable (Short-circuits)General data, non-sensitive logic
crypto.timingSafeEqualConstantNode.js secret/token comparison
hash_equalsConstantPHP secret/token comparison
HMAC HashingConstantComparing high-entropy sensitive keys

Beyond String Comparison

While fixing your comparison logic is critical, remember that side-channel security is a broad field. If you’re handling cryptographic keys, you should also look into Preventing Cryptographic Failures: Secure Node.js & PHP Practices to ensure your entropy and algorithm choices are sound.

Furthermore, timing leaks aren't limited to authentication. I once found a logic flaw in a custom file-processing script that leaked whether a file existed on the disk based on how long the fs.stat call took. If you're dealing with file uploads, make sure you're following the best practices outlined in Preventing Improper File Deserialization: A Guide for Node.js and PHP.

FAQ

Q: Does constant-time comparison make my app slower? A: Technically, yes. It forces the CPU to perform work even after it has found a mismatch. However, the performance hit is negligible—usually measured in nanoseconds—and is a necessary trade-off for security.

Q: Do I need to use constant-time functions for every string comparison? A: No. Only use them for secrets like API keys, password hashes, CSRF tokens, or HMAC signatures. Comparing a username or a UI string doesn't require this level of protection.

Q: Can network jitter hide these attacks? A: Network noise can make timing attacks harder, but it doesn't make them impossible. Attackers use statistical analysis over a large number of requests to filter out network noise and isolate the server's processing time. Don't rely on network latency to keep your secrets safe.

I’m still experimenting with how to handle length-based leaks more gracefully in my own projects. While timingSafeEqual is great, dealing with varying input lengths still requires careful design. For now, I stick to hashing both sides with a shared secret before comparison, which normalizes the length and adds an extra layer of defense.

Similar Posts