Olha Stefanishyna
← Back to home

Timing Attacks: Why Your Code Might Be Leaking Secrets

Cover image for timing attacks prevention overview. AI-generated.
Cover image for timing attacks prevention overview. AI-generated.

Have you ever heard about side-channel attacks? They are a category of security attacks where attackers instead of breaking cryptography directly, analyze unintended information that "leaks" from the physical implementation of a system during normal operation.

Side-channel attacks are dangerous - even perfectly implemented cryptography can be vulnerable because of information leakage from the physical implementation of cryptographic systems. They may be invisible to standard security audits as it requires interdisciplinary knowledge to understand and prevent.

There are several types of the attack: timing attacks, cache timing attacks, power analysis attacks and others.

Timing attacks are one of the most common and accessible side-channel attacks because:

  • They can be performed remotely over networks
  • Don't require physical access to hardware
  • Can be executed with standard software tools
  • Often affect web applications and APIs directly

Understanding timing attacks

Instead of attacking the mathematical solution of your encryption algorithms, these attacks target how your code actually runs, allowing attackers to extract passwords, private keys, and other sensitive data simply by measuring response times. Timing attacks present unique challenges because they can bypass traditional security measures entirely.

In 2003, researchers David Brumley and Dan Boneh demonstrated extracting complete 1024-bit RSA private keys from SSL servers over a network using only timing analysis—requiring about one million queries over two hours. Their attack worked despite network jitter and proved that timing attacks are real danger for real systems.

The core principle is deceptively simple: execution time often correlates with specific properties of the secret like values and length.

For example, consider this condition:

javascript
1'HELLO' === 'AAAA';

It is technically vulnerable because JavaScript engines (V8, SpiderMonkey) implement string comparison with an early exit for performance.

Testing "AAAA" against string "HELLO" fails on the first character (fastest), while "HAAA" fails on the second character (slightly slower), and "HEAA" fails on the third (even slower). An attacker can determine the correct string character-by-character, reducing the search space from exponential to linear complexity.

This is important - it is not a practical example because:

  • Timing differences are tiny - nanoseconds, not milliseconds
  • Network jitter drowns out these tiny differences
  • Nobody compares passwords directly (I hope so)

Let's consider more practical examples.

Real timing attacks exploit operations that take milliseconds, not nanoseconds. When a database lookup or cryptographic operation creates measurable delays, attackers can extract secrets even over noisy networks.

Authentication systems under attack

Authentication systems represent the highest-value target for timing attacks in web applications. When your authentication takes different amounts of time based on whether a username exists, or when your password comparison exits early on the first wrong character, you're broadcasting sensitive information through timing patterns.

javascript
1function authenticate(username, password) {
2 const user = database.getUser(username);
3
4 if (!user) {
5 return false; // Fast return - major timing leak
6 }
7
8 return bcrypt.compare(password, user.hashedPassword); // Slow operation
9}

When an invalid username is provided, the function returns immediately. For valid usernames, the expensive bcrypt.compare() operation executes, creating a timing difference that's easily detectable across network connections.

Attackers can build complete lists of valid usernames before launching targeted password attacks.

API rate limiting and timing analysis

Rate limiting systems, designed to prevent abuse, often become timing attack vectors themselves. Timing-based rate limit detection allows attackers to map rate limiting thresholds and develop evasion strategies:

javascript
1// Rate limiting creates timing patterns
2app.use(
3 rateLimit({
4 windowMs: 15 * 60 * 1000, // 15 minutes
5 max: 100, // Limit requests per window
6 handler: (req, res) => {
7 res.status(429).send('Too many requests'); // Immediate response
8 },
9 })
10);
11
12// Normal request: 200-500ms response
13// Rate limited request: <10ms response (timing leak)

Rate-limited requests typically return immediately with error codes, while normal requests undergo full processing. This timing difference enables attackers to detect rate limiting activation and adjust their attack patterns accordingly.

What to do with the attack on the rate limiting systems? You may see suggestions like hiding rate limits. Because visible rate limiting allows attackers to:

  • Map exact thresholds (e.g., "I can make exactly 99 requests before hitting the limit")
  • Optimize attack patterns to stay just under limits
  • Rotate through different IPs/sessions more efficiently
  • Time their attacks to reset windows perfectly

The idea is that uncertainty forces attackers to be more cautious. However, this is generally considered "security through obscurity" and not very effective - attackers can still discover the limits through probing.

I would suggest to consider rate limits as public information. They just should be sensible, but they may not be secrets.

You may learn more about rate-limiting at Backend Rate Limiting

Modern defensive strategies and countermeasures

Defending against timing attacks requires a multi-layered approach combining constant-time programming, architectural defenses, and monitoring systems. The fundamental principle is ensuring that execution time remains independent of secret data.

Constant-time comparison functions

Constant-time comparison functions are the cornerstone of timing attack defense. Modern platforms provide built-in timing-safe comparison functions.

Dummy‐hash trick for passwords: Make your login endpoint always invoke the slow password check, whether the username exists or not, so an attacker can’t time‐probe “does this user exist?”

javascript
1// Precompute a dummy hash once at startup:
2const DUMMY_HASH = '$2b$12$K9QhMWzVN5YbRz5sXl0ueODhk0PtAyp7cVt2hx6Vj7XOx0JPTWm6W';
3
4// In your login handler, always do bcrypt.compare:
5async function login(username, password) {
6 const user = await db.findUser(username);
7 // If user is missing, compare against the dummy hash
8 const hashToCompare = user ? user.hash : DUMMY_HASH;
9 const passwordValid = await bcrypt.compare(password, hashToCompare);
10
11 if (!user || !passwordValid) {
12 return { error: 'Invalid credentials' };
13 }
14 return { token: generateToken(user) };
15}

Bcrypt’s .compare() is already designed to run in constant time per hash, but if you ever compare raw secrets (HMACs, tokens, API keys, etc.), you should never use ===. Instead do something like

javascript
1const { timingSafeEqual } = require('crypto');
2
3function safeCompare(a, b) {
4 const bufA = Buffer.from(a);
5 const bufB = Buffer.from(b);
6
7 // If lengths differ, compare bufA to itself (constant time)
8 // to prevent leaking the length of the secret
9 if (bufA.length !== bufB.length) {
10 timingSafeEqual(bufA, bufA);
11 return false;
12 }
13
14 // true constant-time compare
15 return timingSafeEqual(bufA, bufB);
16}

Architectural defenses

Architectural defenses are system design patterns that eliminate timing attack vectors by restructuring how your application processes sensitive operations. Instead of fixing timing leaks in code, you redesign the system so timing information becomes meaningless to attackers.

Key Architectural Patterns:

  • Request/Response Decoupling
  • Response Caching Strategy
  • Uniform Processing Pipelines
  • Service Segregation

Let's look closer at Request/Response Decoupling:

javascript
1// app/api/auth/forgot-password/route.js
2import { addToQueue } from '@/lib/queue';
3
4export async function POST(request) {
5 const { email } = await request.json();
6
7 // Queue the task instead of processing inline
8 addToQueue('password-reset', {
9 email,
10 requestId: crypto.randomUUID(),
11 timestamp: Date.now(),
12 });
13
14 // Return response immediately
15 return Response.json({
16 message: "If an account exists, you'll receive reset instructions",
17 });
18}
19
20/** Background worker processes queue (runs separately) **/
21// lib/workers/password-reset.js
22export async function processPasswordReset({ email }) {
23 const user = await findUserByEmail(email);
24
25 if (user) {
26 await sendPasswordResetEmail(user);
27 }
28 // No timing information leaks to the original requester
29}

Monitoring systems

Monitoring systems are detection and analysis tools that identify timing attack attempts by analyzing request patterns, response times, and behavioral anomalies. Unlike rate limiting systems that prevent requests, monitoring systems are designed for detection. They observe and alert on suspicious patterns without blocking the traffic, allowing you to analyze potential attacks.

Start protecting your code today

Defending against timing attacks starts with three simple changes to your codebase:

  • Switch all secret comparisons to timing-safe functions
  • Implement dummy processing for authentication failures
  • Use libraries with built-in timing attack protection

Timing attacks are subtle but real. They won't show up in your unit tests or security scans. The best defense is understanding how they work and building protection into your code from the start by incorporating timing attack considerations into architectural decisions, development processes, and monitoring systems.

Conclusion

While the fundamental attack principles remain unchanged since Paul Kocher's pioneering work in 1996, timing attacks now affect a much broader range of applications - from browser JavaScript to backend services.

Success in defending against timing attacks requires combining multiple defensive layers: constant-time implementations at the code level, architectural protections like rate limiting and response normalization, comprehensive monitoring for attack detection, and regular security assessments to identify emerging vulnerabilities. By implementing these defenses systematically, development teams can build applications that resist even sophisticated timing attacks while maintaining the performance and functionality that users expect.

Also published on:

Let's talk