CWE-208: Observable Timing Discrepancy
Overview
An observable timing discrepancy occurs when the time an operation takes reveals something about a secret value it processed - most often a comparison that exits as soon as it finds a mismatch, or a code path that only does expensive work when a guess happens to be partially correct. An attacker who can trigger the operation repeatedly and measure how long it takes can narrow the secret down without ever seeing it directly. How far that goes depends on what leaks: a hand-written loop that returns on the first mismatch can be walked a byte at a time, while a built-in content comparison leaks at a coarser granularity than that, and the section below sets out why the byte-at-a-time story is the wrong model for most reported findings. This shows up most often in password, token, HMAC, and signature verification, and in login flows where the work done differs depending on whether an account exists.
Relationship to Other CWEs
CWE-208 is closely related to CWE-385 (Covert Timing Channel), and the two are frequently reported for the same underlying code: a secret comparison or secret-dependent branch whose duration leaks information. CWE-385 is the general information-leak-via-timing-channel weakness; CWE-208 is specifically about the observable discrepancy itself - the measurable difference in execution time or response behavior.
The two do not share a remediation, and the difference decides which fix to reach for. A comparison that walks a secret is fixed by swapping in a constant-time primitive. A branch that does different amounts of work depending on a secret - a login that skips password hashing when the username does not exist - is not helped by that at all, because the comparison it reaches is already constant-time and the leak happened before it. That case is fixed by equalizing the work, and CWE-385 is where it is written up. A finding filed as CWE-208 against a login endpoint is usually the second case, so read the code before choosing the fix rather than reading the CWE number.
Risk
Medium-High: Timing discrepancies let an attacker enumerate valid usernames and, where a secret is compared directly, recover it through repeated measurement - an API key, a session token or a CSRF token is both the thing compared and the thing worth stealing. A signature check is different: what leaks there is the expected MAC for the message the attacker chose, not the signing key, so the payoff is forging that one request rather than being able to sign anything. Either way the endpoint can be walked toward acceptance without triggering an obvious error or leaving an artifact more suspicious than a slightly slower response. Exploitation typically requires many requests to average out network jitter, but is practical both locally and, with enough samples, over a network.
Remediation Steps
Core Principle: Any code path that touches a secret value must take the same amount of time and access the same memory regardless of what the secret is, so timing carries no information about it.
Trace the Data Path
- Source: A secret value - password hash, HMAC digest, API key, session token, cryptographic signature, or private key material
- Sink: An externally observable timing signal - HTTP response latency, RPC round-trip time, or any other measurable delay an attacker can trigger repeatedly
- Data Flow / Missing Controls: A comparison, loop, or branch whose duration or memory access pattern depends on how much of the secret matches, with no control that equalizes timing across outcomes
Use Constant-Time Comparison for Secrets (Primary Defense)
Replace any equality check on a secret value with a comparison primitive whose duration does not vary with how much of the input matched. The exact contract differs by language - some compare to the end of one argument, some refuse unequal lengths outright - so read the contract for your language before relying on it; what they share is that no prefix of a correct guess is answered faster than a wrong first byte.
- Apply this to every comparison involving a secret: password hashes, HMAC signatures, API keys, session tokens, CSRF tokens
- Never use a language's default equality operator, string comparison method, or a hand-rolled loop with an early
return falsefor this purpose - Most mainstream languages ship a constant-time comparison primitive - use it instead of writing your own. C is the notable exception, with no portable standard function; see CWE-385 for the accumulated-XOR form
Equalize Timing Across Secret-Dependent Branches (Secondary Defense)
A constant-time comparison alone isn't enough when the surrounding logic branches on whether the secret exists at all - for example, a login flow that skips password verification entirely when the username doesn't exist, making "no such user" measurably faster than "wrong password." Perform equivalent work on every branch, such as running the expensive verification step against a fixed dummy value when the fast path would otherwise skip it.
This branch, not the comparison, is where the large discrepancies are. A comparison leaks at the scale of nanoseconds; skipping a password hash leaks at the scale of the hash - tens or hundreds of milliseconds, measured on the CWE-287 language pages at between roughly 1,000x and 138,000x depending on the stack, against a background of network jitter measured in milliseconds. Two things follow. A finding on a login, token-redemption, password-reset or API-key endpoint is worth checking for the skipped-work branch before the comparison, because that is the one an attacker can exploit from the internet rather than from the same machine. And the dummy has to be a genuine value at full cost: every password library rejects a malformed or empty stand-in without hashing, which reopens the gap while looking like the fix. CWE-287 carries the worked login examples per language, with the measured before-and-after figures.
What Leaks Is Coarser Than "One Byte at a Time"
The hand-written loop under Common Vulnerable Patterns below does leak per byte, but most reported findings are a language's built-in content comparison - String.equals, Arrays.equals, Buffer.equals, SequenceEqual, memcmp under == - and those do not compare a byte at a time. They compare a machine word or a SIMD register at a time, so the early exit lands on a block boundary and every position inside one block takes the same time. Whether == is one of these is per-language: it compares contents in Python and JavaScript, and in C# only when the static type is string, while in Java and for a C# byte[] it compares references and therefore leaks nothing - it is simply wrong. The language pages give each case. Measured across the four runtimes the language pages below cover, with a 64-byte secret compared in-process, the entire spread between a mismatch in the first byte and a mismatch in the last is between 0.4 ns and 4.5 ns, and two positions a few bytes apart are not separable at all. Recovering the secret one character at a time by timing one of these comparisons is folklore rather than a description of them.
That does not make it a false positive, and the fix does not change:
- The block width is an unspecified implementation detail. It varies with the runtime, the JIT, the CPU's vector width and the string's internal encoding, so an application that is unexploitable today can become exploitable on the next platform upgrade with no code change.
- What survives at every block width is the length check. Every one of these APIs compares lengths first, which is a clean, easily measurable signal.
- An attacker on the same host, in a co-tenanted VM or in the same browser process measures far finer than one over a network, and the fix costs one function call.
What the coarseness should change is where you look first when triaging a real finding: the unequal-work branch above, not the comparison.
Add Rate Limiting and Monitoring (Defense in Depth)
- Rate-limit and log repeated near-miss attempts against authentication and verification endpoints, since exploiting a residual timing signal requires many trials
- Do not rely on a random delay or added jitter as the primary fix - an attacker can average out random noise over enough samples, so the variance has to be removed at its source, not masked
Test with Malicious Inputs
- Send the same request many times with a correct secret and many times with an incorrect one, then compare the mean and variance of response time - a real leak shows up as a statistically significant difference
- Test the specific scenario in the finding: does a request for a valid identifier (existing user, valid token prefix) respond measurably differently than an invalid one?
- Re-scan with the security scanner to confirm the finding is resolved
Common Vulnerable Patterns
// VULNERABLE - pseudo-code
function verify_token(provided, expected):
if length(provided) != length(expected):
return false // fast rejection reveals a length mismatch
for i in range(length(provided)):
if provided[i] != expected[i]:
return false // exits sooner the more bytes mismatch first
return true
// Attack: submit many guesses, measure response time per guess
// Result: guesses that match more of the secret's prefix take measurably longer,
// letting the attacker recover the secret one byte at a time
Why this is vulnerable: the function returns the correct answer every time. What leaks is not the result but how long producing it took, and that is not part of the return value, not asserted by any test, and not recorded in any log. The early return false that makes the comparison efficient is the same line that makes each byte of the secret independently measurable.
The usual reason a finding here gets dismissed is that the signal - tens of nanoseconds per byte - sits far below the jitter of a network round trip. That reasoning does not hold, because the attacker is not trying to measure a single request. The timing difference is systematic and the jitter is not, so averaging over enough samples separates them. The question worth asking of a finding is therefore not whether the difference is large, but whether the operation can be repeated on demand with an attacker-chosen input. If it can, the size of the difference only sets how many requests it costs.
Secure Patterns
// SECURE - pseudo-code
function verify_token(provided, expected):
return constant_time_equals(provided, expected) // always compares every byte, no early exit
Why this works: A constant-time comparison performs the same work whatever the two values contain, so its duration depends on their length and never on how many bytes happen to match. There is no early exit for an attacker's timing measurement to key off, so repeated probing yields no statistically useful signal about the secret's contents. Feeding it two values of different lengths is where the language-level differences live, and the per-language pages give the contract for each.
Common Pitfalls
- Adding a random delay instead of fixing the comparison: Padding every response with jitter raises the number of samples an attacker needs, but does not remove the underlying signal - enough requests still let an attacker average out the noise and recover the timing difference.
- Fixing the comparison but leaving an unequal-work branch upstream: Switching to a constant-time comparison function stops leaking the secret's content, but a login flow that still skips password verification entirely for a nonexistent user leaks the secret's existence (a valid username) through the same timing channel.
- Checking length before calling the constant-time function: A separate
if length(a) != length(b): return falsein front of an otherwise-correct constant-time comparison reintroduces a timing branch. This is usually acceptable for fixed-length values like hashes and HMAC digests, but not when the length itself must stay secret.
Language-Specific Guidance
- C# -
CryptographicOperations.FixedTimeEqualsand ASP.NET Core patterns - Java -
MessageDigest.isEqualand Spring Security patterns - JavaScript -
crypto.timingSafeEqualand Node.js/Express webhook verification - Python -
hmac.compare_digestand Django/Flask patterns
Additional Resources
- BearSSL - Constant-Time Cryptography - which operations are and are not constant-time, and why
- CWE-208: Observable Timing Discrepancy
- Go
crypto/subtle- reference semantics for a constant-time comparison API - OWASP Authentication Cheat Sheet
- OWASP Testing Guide - Testing for Account Enumeration - how to reproduce the login-timing case