Timing Side-Channel Vulnerabilities in Authentication Code

Attackers can recover secrets one character at a time by measuring response delays.

Correspondent · · 10 min read
Cover illustration for “Timing Side-Channel Vulnerabilities in Authentication Code”
Cryptographic Failures · October 2, 2026 · 10 min read · 2,272 words

Timing side-channel vulnerabilities in authentication code break a basic assumption most developers carry into every code review: that correct output means correct code. These bugs accept the right user, reject the wrong one, and produce no error anywhere in the logic. The danger sits entirely in how long the code takes to run, not in what it decides.

Why timing side-channels are different from ordinary authentication bugs

A standard authentication bug appears as a wrong decision: a password that should fail gets accepted, or a token that should be rejected slips through. Timing side-channels don't work that way. The comparison returns the correct answer every single time. Nothing in the control flow is broken, and nothing in the output is wrong. What leaks is the clock. When a comparison exits early on a mismatch, the response comes back faster than it would if the comparison had run longer, and that speed difference carries information a careful observer can read. Secret data shaping either the path the code takes or the memory addresses it touches is enough to count as a timing vulnerability, a violation of what's called constant-time behavior. This sits outside the usual security checklist. No memory gets corrupted, nothing gets injected, no privilege gets escalated. An attacker just needs a stopwatch and patience.

How the early-exit comparison leaks secret information

Diagram: How Character-by-Character Timing Leaks the Full Secret. Visualizes: Illustrate the early-exit timing attack as a stepped sequence: an attacker tries every possible first character, identifies the one that takes slightly longer to reject…

Picture a simple equality check: compare a guessed value against a secret, one character at a time, and stop the moment the characters don't match. That stopping point is the leak. Soumyadeep Dey's walkthrough lays out the pattern: a guess whose first character is wrong gets rejected almost instantly, because the loop exits on the very first comparison. A guess that gets the first character right but fails on the second takes slightly longer, because the loop made it one iteration further before bailing out. Every additional correct character at the front of the guess adds a small, measurable delay before rejection.

That small delay is enough to turn a brute-force problem into something far more manageable. An attacker doesn't need to guess an entire password or token at once. Instead, the attacker tries every possible first character, watches for the one that takes slightly longer to reject, locks that character in, and moves to the second position. Repeat the process down the length of the secret: the search space collapses from exponential to linear, one character solved at a time rather than the whole string guessed in one shot. The same structural flaw occurs wherever a secret gets checked with an early-exit equality test, as in OpenVPN 2.3.0 and earlier running in UDP mode, which used memcmp() to compare HMAC tokens, a comparison that is not constant-time and allowed remote attackers to obtain sensitive information.

Where this flaw appears in real authentication code

This isn't confined to toy examples. It appears repeatedly in production code, across different languages and different protocol layers. No particular tech stack gets to assume it's safe by default.

CVE-2025-59432, found in the ongres/scram Java library, is a clean illustration. The library used Arrays.equals to compare client proofs and server signatures during SCRAM authentication, a method with no constant-time guarantee. The fix, shipped in version 3.2, swapped that call for MessageDigest.isEqual, which does perform constant-time comparison. The advisory's own description of the impact was blunt: every user relying on SCRAM authentication through that library was affected.

OpenVPN carried a similar flaw in versions 2.3.0 and earlier when running in UDP mode. The code used memcmp() to compare HMAC tokens, and because memcmp() is not constant-time, remote attackers could use the timing differences to recover sensitive information. The open62541 OPC UA library had its own version of the same problem: its UA_ByteString_equal() function reduced, in practice, to a plain memcmp() call for MAC comparison, returning as soon as it hit the first differing byte, which let a remote peer recover a valid MAC one byte at a time.

The pattern isn't a relic of older codebases, either. In February 2026, a commit by Filip Skokan patched a timing side-channel in Node.js's Web Cryptography implementation, fixing HMAC and KMAC verification by replacing memcmp with CRYPTO_memcmp. And a 2026 open-source SaaS starter kit, the kind of template thousands of new projects get built on top of, shipped by default without constant-time cryptographic comparisons or tight RPC permissions. Fixing it took a dedicated security-hardening pull request after the fact. Different languages, different decades, same root cause: a comparison function chosen for convenience rather than for its timing guarantees.

Why the "network jitter makes this unexploitable" objection fails

The most common reason teams deprioritize this class of bug is the assumption that real-world network noise, the small random delays introduced by routers, load balancers, and the internet in general, buries any nanosecond-level timing signal before an attacker can read it. That assumption doesn't hold up against the evidence.

Lucky 13 is the clearest counterexample. It's a remote attack against CBC-mode encryption in TLS and DTLS, and it works by exploiting a timing side-channel caused by the variable number of MAC hash function calls needed depending on padding length. Despite running over real networks with all their normal jitter, Lucky 13 recovered plaintext successfully. A later variant, nicknamed "Lucky microseconds," pushed the point further: even after Amazon's s2n-tls implementation had two separate protections applied against this class of attack, researchers still achieved remote, complete plaintext recovery from CBC-mode cipher suites.

The mechanism that makes this possible is statistical. A single request doesn't need to produce a clean, unambiguous timing measurement. An attacker who sends the same request thousands of times and averages the results watches jitter cancel out while the real signal, the consistent microsecond-level difference caused by the secret, stays put. Signal-to-noise improves with repetition, not with how close the attacker sits to the server. Even the SCRAM advisory discussed earlier acknowledges this directly: it notes that exploitation requires high precision and many repeated attempts, and then concludes that the only reliable fix is a change to the code itself, not any network-level control.

The compiler-level problem that voids correctly written constant-time code

Writing the comparison correctly at the source level turns out not to be enough on its own. A developer can write exactly the kind of branch-free, constant-time logic that security guidance recommends, and the compiler can still quietly undo it during optimization.

CVE-2025-66442, found in Mbed TLS through version 4.0.0 and TF-PSA-Crypto through version 1.0.0, is the clearest example of this. The flaw isn't in how the cryptography was designed or written. It lives in LLVM's select-optimize compiler pass, which takes constant-time select instructions, the kind of branch-free logic developers write specifically to avoid timing leaks, and rewrites them into conditional branches for the sake of performance. As a result, RSA and CBC/ECB decryption operations end up running in variable time, even though the original source code was written to be constant-time. This gets classified under CWE-385, Covert Timing Channel, and it's attributed to compiler behavior rather than any mistake in the implementation itself.

Intel's own guidance for developers writing constant-time code in C or hand-written assembly says the only way to be sure is to verify the compiled output in the actual environment where it will run. Keywords like volatile, register, and inline are hints to the compiler, not guarantees, and while they sometimes help block unwanted optimizations, they can't be relied on across compilers.

Managed and dynamic languages carry a version of this same risk, often with fewer tools to fight it. Python, for instance, offers limited or no way to hint the JIT compiler about preserving constant-time behavior, which makes those guarantees much harder to reason about in the first place. CVE-2025-29780, found in the Post-Quantum Secure Feldman VSS library, shows what that looks like in practice: the advisory itself acknowledges that the timing vulnerabilities in that library can't be adequately fixed in pure Python, and no patched version existed at the time the advisory was published. Source-level correctness and compiled-binary correctness are two separate problems, and fixing one doesn't guarantee the other.

Why security reviews miss timing vulnerabilities in authentication code

Given how subtle this bug class is at the source level and how unreliable compilers can be at preserving it, it's worth asking why standard security testing doesn't catch it more often. Most of the tooling and process built for security review simply isn't aimed at this problem.

A USENIX Security paper, titled "These results must be false": A Usability Evaluation of Constant-Time Analysis Tools, tested the existing landscape of detection tools, including Binsec, ct-verif, dudect, valgrind, MemSan, and haybale-pitchfork. Practitioners found the output of these tools so confusing that the researchers named the paper after their reaction to it. The tools that take a more formal approach, like BLAZER, CacheAudit, FlowTracker, and CT-LLVM, can in principle prove that no leakage exists, but they generally can't produce a concrete example of a violation when one is found, and they can carry high false-positive rates (FlowTracker is something of an exception, since it can return counterexamples in some cases). Statistical tools like dudect are easier to pick up and use, but they can't prove the absence of a leak the way the formal tools claim to.

Mainstream web application penetration testing tools don't fill that gap either. Burp Suite Pro, OWASP ZAP, and Nuclei, the backbone of most commercial pen-testing engagements, don't natively detect timing oracle vulnerabilities in authentication code. A pen test built around black-box HTTP responses, without direct access to the source code behind the authentication comparison logic, is unlikely to catch a timing side-channel at all. Catching this class of bug takes direct, human review of the comparison logic itself, with access to the source, rather than a scan of the API surface from the outside. That's the gap between a compliance-driven pen test checking boxes and a review built to actually find what's wrong.

How timing attacks have reached post-quantum cryptography

A reasonable instinct might be to treat this whole problem as temporary, something that gets resolved once the industry finishes migrating from classical cryptography to post-quantum algorithms. That instinct doesn't hold up. The vulnerability class travels with the math, not with any particular algorithm.

The Kyberslash attack, published in IACR TCHES 2025, exploits secret-dependent division timings inside Kyber implementations. Kyber is one of the leading post-quantum algorithm candidates, and Kyberslash shows it carries the exact same structural weakness as classical implementations. The mechanism driving it is identical to everything described above: a computation whose execution time depends on a secret value, measured remotely through repeated queries. Teams planning a post-quantum migration can't treat constant-time verification as a problem the new algorithm design solves for them. The same scrutiny applied to classical implementations has to get applied again to whatever replaces them.

Constant-time comparison: requirements and common mistakes

Diagram: The Three Conditions Constant-Time Code Must Survive. Visualizes: Show three requirements that must all hold simultaneously — (1) runtime independent of secret value, (2) execution path independent of secret value, (3) memory access…

Fixing this properly means satisfying three conditions at once: the comparison's runtime has to stay independent of the secret value, the code's execution path has to stay independent of the secret value, and the pattern of memory accesses has to stay independent of the secret value. All three conditions have to survive the trip through the compiler, not just exist in the source file. The Pitchfork paper shows what the fix looks like in practice: instead of comparing byte by byte and exiting the moment a mismatch appears, the corrected version accumulates the XOR difference across every single byte, with no branching and no early exit tied to the secret, and only returns the accumulated result at the end. Every byte gets compared no matter where the first mismatch happens to fall.

Most major platforms already provide a safe primitive that does this correctly, and the fixes described earlier in this piece lean on exactly those tools. Node.js offers CRYPTO_memcmp, the function used in the February 2026 HMAC and KMAC fix. Python offers hmac.compare_digest(a, b) as the safe choice, though it's worth knowing that older Python versions, through 3.9.1, carried their own bug in this exact function: CVE-2022-48566 allowed constant-time-defeating optimizations inside compare_digest itself, a reminder that even the safe primitive needs to be running a patched version to actually deliver on its promise. Java offers MessageDigest.isEqual, the function that replaced Arrays.equals in the ongres/scram fix. C code linked against OpenSSL has a constant-time comparison function available too, the same kind of function used in the Node.js fix, while the open62541 project chose a different constant-time function of its own, UA_constantTimeEqual(), to solve the same need.

Knowing which function to call doesn't eliminate every way to get this wrong. A common mistake is calling a genuinely constant-time comparison function, but checking the lengths of the two buffers first and exiting early if they don't match. Length is itself secret-dependent information, and branching on it before the comparison even starts throws away the protection the comparison function was supposed to provide. Another common mistake is writing a custom XOR accumulation loop by hand, the pattern described above, in a language or compiler that doesn't preserve that structure through optimization, hitting the same compiler-level rewriting described earlier in this piece. Some implementations pad automatically to handle that case safely, and others don't, so the behavior has to be checked for the specific function and platform in use.

For HMAC workflows, including a timestamp in the signed payload, rejecting requests older than five minutes, and validating the timestamp before the HMAC comparison closes off a different angle of attack: it stops timing measurements from being accumulated across a replay window. The comparison function handles the leak inside a single request. The timestamp check limits how much an attacker can gather across many of them.

Sources

  1. CVE-2025-66442: Mbed TLS Timing Side-Channel Vulnerability
  2. Timing Attack Vulnerability in SCRAM Authentication
  3. Timing-based side-channel vulnerability in authentication system
  4. Finding and Eliminating Timing Side-Channels in Crypto Code with Pitchfork
  5. CVE 2025 29780
  6. Compiler-induced constant-time violations (CVE-2025-66442) — Mbed TLS documentation
  7. Guidelines for Mitigating Timing Side Channels Against Cryptographic Implementations

More in Cryptographic Failures