Join our Newsletter — 33% off our NHI Course
Home› Guides› Timing Attacks Guide: Safe Secret Comparison in Application…
Guide Application Security

Timing Attacks Guide: Safe Secret Comparison in Application Code

PDF for newsletter subscribers · free to read online

← All guides
By NHI Mgmt Group, edited by Lalit Choda Updated 14 November 2026 15 min read
On this page

An ordinary equality check on a secret, such as an API key, a webhook signature or a reset token, can tell an attacker how close their guess is simply by how long the check takes. Application timing leaks are not exotic: CWE lists real products that compared HMAC signatures with a plain string equals, and login pages that answered faster for unknown usernames. This guide covers the application layer only: comparing tokens, HMACs and password hashes safely, removing timing differences from login and reset flows, how realistic remote measurement is, and how to find and test for these flaws. It is written for application developers, AppSec engineers and code reviewers. CPU, cache and physical side channels are covered in the Side-Channel Attacks Guide, and this page does not repeat them.

Key takeaways

  • Never compare secrets with ==. Use the platform's constant-time function: hmac.compare_digest in Python, crypto.timingSafeEqual in Node.js, MessageDigest.isEqual in Java, CryptographicOperations.FixedTimeEquals in .NET, subtle.ConstantTimeCompare in Go, hash_equals in PHP.
  • Length still leaks. Every one of those functions documents that a length mismatch is handled early. Compare fixed-length values (digests), not raw variable-length secrets.
  • The comparison is rarely the only leak. Database lookups by token, early returns for unknown users and skipped password hashing leak more time than a byte comparison ever will.
  • Remote measurement is practical. Concurrency-based "timeless" timing attacks published at USENIX Security 2020 detected differences as small as 100 nanoseconds over the network, so "the internet is too noisy" is not a defence.
  • Find it with code review and lint rules, prove it with statistics. Static rules catch equality on secret-named variables; a fixed test of many timed requests per case confirms that login and reset paths are flat.

How application timing leaks work

A timing leak exists when two code paths that should look identical to an outsider take measurably different time, and the difference depends on a secret. CWE classifies this as CWE-208 Observable Timing Discrepancy, a child of CWE-203 Observable Discrepancy. At the application layer there are three common shapes.

  • Early-exit comparison: a byte-by-byte equality check stops at the first mismatch. The more leading bytes of a guess are correct, the longer the check runs, so an attacker can in principle recover a secret one position at a time instead of guessing the whole value. CWE-208 cites CVE-2019-10071, where a framework compared HMAC signatures with String.equals() rather than a constant-time comparison.
  • Branch on existence: the code returns as soon as it learns that a user, email or record does not exist, and only does expensive work (password hashing, sending an email, writing an audit row) when it does. CWE-208 lists several username discovery cases of this kind, including CVE-2003-0637 and CVE-2004-1602.
  • Secret-dependent lookups: searching a database, cache or map by the secret itself. The lookup cost can vary with how much of the key matches, and the result branch (found or not found) usually adds a much larger difference.

The defender's goal is not to make code fast or slow. It is to make the time an outsider can observe independent of the secret. That is the same principle the Side-Channel Attacks Guide applies to cryptographic libraries, applied here to ordinary request handling.

Where these leaks appear in real applications

LocationSecret at riskTypical leaky patternSafe pattern
Webhook receiversShared HMAC signing key (via forged signatures)Computed signature compared to the header with ==Recompute HMAC, compare digests with a constant-time function; GitHub's webhook docs explicitly warn against ==
API key and bearer token checksThe key or tokenKey used directly as a database lookup value, then compared with ==Look up by a non-secret identifier, then constant-time compare a stored hash of the secret part
Session cookies and signed URLsSignature or MACCustom signature check written by handFramework's signing helper, or constant-time compare of the MAC
Password loginWhether an account existsReturn early when the username is not found; hash only for real usersRun the same hash work on every request, including a dummy hash for unknown users
Password reset and registrationWhether an email is registeredSend email synchronously only for known accountsSame response and same timing for every request; queue the send asynchronously
MFA and one-time codesThe codeString equality on the submitted codeConstant-time compare plus strict attempt limits
Internal service-to-service authShared secrets, client secretsStatic secret compared with == in middlewareConstant-time compare, or replace shared secrets with mTLS or signed tokens

Many of these secrets belong to machines rather than people: API keys, webhook signing keys and client secrets are non-human identity credentials, and a timing leak against them is a credential exposure. See the API Key Management Guide and the Webhook HMAC Signing vs mTLS vs IP Allowlisting guide for the wider controls around those credentials.

Constant-time comparison functions by language

Every mainstream platform ships a comparison function designed not to stop at the first differing byte. The documented behaviour, checked against each project's own documentation in November 2026, differs mainly in how it treats inputs of different length.

PlatformFunctionInput typesDifferent lengthsDocumented caveat
Python (3.3+)hmac.compare_digest(a, b)Both ASCII str, or both bytes-likeReturns False; length and type may leak, values do notMixing str and bytes is an error; uses OpenSSL CRYPTO_memcmp where available from 3.10
Node.js (v6.6.0+)crypto.timingSafeEqual(a, b)Buffer, TypedArray, DataView, ArrayBufferThrows an errorDoes not make surrounding code timing safe; float arrays can give unexpected results
Javajava.security.MessageDigest.isEqual(a, b)byte[]Returns falseTime depends only on the length of the first argument
.NETCryptographicOperations.FixedTimeEquals(left, right)ReadOnlySpan<byte>Returns false immediately (the only short circuit)Use instead of SequenceEqual for MAC tags
Gocrypto/subtle.ConstantTimeCompare(x, y)[]byteReturns 0 immediatelyRun time depends on length, not contents
PHP (5.6+)hash_equals($known, $user)stringReturns false immediately; known length may leakThe user-supplied value must be the second argument
Ruby on RailsActiveSupport::SecurityUtils.secure_compare / fixed_length_secure_compareStringsecure_compare returns false; fixed_length_secure_compare raisesSecret length can be determined
  • Compare digests, not raw secrets: because length is handled early everywhere, compare values of a fixed length. For HMAC verification this is natural: both sides are digests of the same algorithm. For a stored token, store a hash of it and compare the hash of the submitted value with the stored hash. Hashing first also removes the Node.js exception path for mismatched lengths.
  • Normalise encoding before comparing: decode hex or base64 to bytes on both sides, or encode both the same way. Comparing a lowercase hex string with an uppercase one, or a "sha256=" prefixed header with a bare digest, makes a correct signature fail and tempts developers back to hand-written comparisons.
  • Prefer the higher-level helper: a framework's signed cookie, CSRF or token verification helper already compares safely. Hand-written verification code is where most findings come from.
  • Do not write your own loop: an XOR-accumulate loop looks constant time in source code but compilers and JIT runtimes can optimise it. Go's documentation is explicit that its data-independent timing mode only helps code that is already written with constant-time operations. Use the library function.

Django's constant_time_compare lives in django.utils.crypto, which Django's documentation does not list among its stable public utilities, so check the source for your version or use hmac.compare_digest directly.

Removing timing differences from login, reset and lookup flows

The comparison function only protects the last few microseconds. The bigger differences come from what the request does before and after it. The Account Enumeration Prevention Guide covers response wording, status codes and bot controls; this section covers the timing side.

  • Login: the OWASP Authentication Cheat Sheet shows the classic "quick exit" flaw, where an unknown user returns before any password hashing, and notes the difference can reach seconds with slow hashes. Run the same work on every path: when the account is not found, verify the submitted password against a fixed dummy hash created with the same algorithm and parameters as real hashes. Keep that dummy hash's parameters in step when you raise work factors, or the gap returns. See the Password Hashing and Key Derivation Guide.
  • Password reset and sign up: the OWASP Forgot Password Cheat Sheet asks for a consistent message and a consistent response time for existing and non-existent accounts, using asynchronous calls or identical logic. In practice, put the "send email" job on a queue for every request and let the worker discard requests for unknown addresses.
  • Token lookup: do not query the database by the full secret. Split the token into a non-secret selector used for the lookup and a secret verifier whose hash is compared in constant time. The lookup then reveals only whether a selector exists, which an attacker cannot use to learn the verifier.
  • Locked and disabled accounts: evaluate lockout and disabled status after the credential work, not before, so these states do not return faster than a wrong password.
  • Error handling: exceptions are slow. A decode error on a malformed signature should take the same route as a well-formed wrong signature, ending in one generic rejection.
  • Artificial delay: a random sleep adds noise that averaging removes, so it is not a fix on its own. Padding every response to a fixed minimum duration can hide residual differences but costs latency and capacity; use it only where the work genuinely cannot be equalised.

How realistic remote timing attacks are

Teams often dismiss timing findings because network jitter dwarfs a few nanoseconds of comparison time. That assumption is weaker than it was.

  • Statistics beat jitter: jitter is noise, and noise averages out over many measurements. An attacker who can send many requests can detect differences far smaller than a single request's variance. Rate limits therefore help directly, which is why the OWASP Authentication Cheat Sheet treats brute-force protection as part of enumeration defence.
  • Concurrency removes much of the noise: the "Timeless Timing Attacks" paper (Van Goethem, Pöpper, Joosen and Vanhoef, USENIX Security 2020) sent requests so they arrive together, via packet coalescing and concurrent handling, and read which response came back first. The authors reported roughly a 100-fold improvement over typical internet timing attacks and detected differences as small as 100 nanoseconds, against HTTP/2 web servers, Tor onion services and EAP-pwd.
  • Co-located attackers are closer still: a tenant in the same cloud region or a compromised service inside your network sees far less jitter than an internet attacker.
  • Size of difference matters: a byte-comparison leak is small and needs many requests; a skipped password hash is milliseconds to seconds and can be seen in a handful of requests. Fix the large differences first.

Choosing the right fix

SituationPreferred approachTrade-off
Verifying a MAC or signatureLibrary verify function, or constant-time compare of fixed-length digestsNone worth noting; this should be universal
Checking a stored API key or tokenSelector plus hashed verifier, constant-time compare of hashesRequires a token format change and migration of existing keys
Login for unknown usersDummy hash verification on every missSpends hashing capacity on attack traffic; pair with rate limiting
Reset and registration emailsAsynchronous queue for every requestAdds a worker and queue to operate
Residual differences that cannot be equalisedPad to a fixed minimum response timeLatency cost on every request; fails if work exceeds the floor
Legacy shared secrets between servicesReplace with mTLS or signed short-lived tokensLarger project, but removes the static secret entirely

Small organisations: the highest value step is a one-off search for == and equals() on secret-named variables, plus using framework helpers. Large organisations: add a lint rule to CI so the pattern cannot return, publish a shared verification library per language, and include timing checks in authentication test suites.

Implementation and testing

  1. Inventory secret comparisons: search code for comparisons involving names such as signature, hmac, token, apikey, secret, otp and digest, and for webhook and middleware verification code. Static analysis tools such as the Semgrep Guide and CodeQL Guide cover custom rules for this pattern.
  2. Replace comparisons: swap each for the platform constant-time function, comparing decoded fixed-length bytes. Keep the change small and reviewable; the behaviour for correct inputs should not change.
  3. Fix flow-level leaks: add dummy hashing on unknown users, move email sending to a queue, and move token lookups to a selector.
  4. Test statistically: in a test environment close to production, send a large fixed number of interleaved requests for each case (valid user with wrong password, unknown user, locked user; correct prefix versus wrong prefix for a token) and compare the distributions, not single timings. Interleaving cases cancels drift from load and caching. A visible gap in medians or a significant distribution test is a failure.
  5. Guard in CI: enable the lint rule as blocking for new code once existing findings are fixed, and keep the timing tests for login and reset in the regression suite.
  6. Roll back safely: comparison changes are low risk, but token format changes are not. Support old and new token formats in parallel during migration, with the old path still using a constant-time compare, and retire the old format on a dated plan.

Detection and measurement

Timing probes look like high volumes of near-identical requests: many login attempts against varied usernames with similar passwords, many reset requests for different addresses, or bursts of requests with signatures that share a prefix. Log authentication failures with a reason code (internally only, never in the response), source, and target, and alert on volume and on concurrent bursts from one client. HTTP/2 connections carrying many simultaneous requests to an authentication endpoint deserve particular attention given the concurrency research above.

MetricWhat good looks like
Open static analysis findings for non-constant-time secret comparisonZero, with the rule blocking in CI
Median response time gap, known versus unknown user (login, reset, sign up)Within normal run-to-run variation in the timing test
Share of API keys and tokens using selector plus hashed verifierRising to 100% on a dated plan
Authentication endpoints covered by per-account and per-source rate limits100%
Timing regression tests in the authentication suitePresent and passing on every release

Common failure patterns

  • Constant-time compare on a variable-length raw secret: the length leaks by design of every library function, and in Node.js the mismatch throws an exception that takes a different path. Compare hashes.
  • Safe compare after a leaky lookup: the token is found by querying the database for the full value, then compared safely. The query already leaked whether it exists.
  • Argument order mistakes: PHP's hash_equals expects the known value first and the user value second, and Java's MessageDigest.isEqual timing depends on the first argument's length. Swapping them weakens the guarantee.
  • Dummy hash out of date: real hashes move to a higher work factor or Argon2id but the dummy hash for unknown users still uses old parameters, reopening a measurable gap.
  • Fast exit for locked accounts: lockout checked before the password work, so locked accounts answer faster than active ones and reveal both existence and lock status.
  • Hand-rolled "constant-time" loops: a custom comparison that the compiler or JIT optimises back into an early exit. Use the platform function.

Framework mapping

FrameworkReferenceRelevance
OWASP ASVS 5.0V11.2.4Cryptographic operations, including comparisons, must be constant time with no short-circuit operations
OWASP ASVS 5.0V6.3.8Valid users must not be deducible from failed authentication, including from response times
CWECWE-208, CWE-203, CWE-204Observable timing discrepancy; observable discrepancy; observable response discrepancy
MITRE CAPECCAPEC-462 Cross-Domain Search Timing; CAPEC-112 Brute ForceAttack patterns that timing leaks enable or accelerate
NIST SP 800-53 Rev 5SC-13, IA-6, AC-7, SA-11Cryptographic protection; authentication feedback; unsuccessful logon attempts; developer testing
NIST CSF 2.0PR.PS-06Secure software development practices integrated into the life cycle
MITRE D3FENDD3-MAN Message AuthenticationVerifying MACs safely is part of implementing message authentication; see the D3-MAN Message Authentication guide

Practitioner checklist

  • Search every codebase for == or equals() on signatures, tokens, API keys, OTPs and digests, and replace each with the platform constant-time function.
  • Compare decoded, fixed-length digests rather than raw secrets, and put the known value first where the library requires it.
  • Store hashes of API keys and tokens, look them up by a non-secret selector, and compare the verifier hash in constant time.
  • Run dummy password verification for unknown users with the same algorithm and parameters as real hashes, and update it when parameters change.
  • Queue reset and verification emails for every request so known and unknown addresses respond identically.
  • Check lockout and disabled status after credential work, and send all failures down one generic rejection path.
  • Apply per-account and per-source rate limits to every authentication and verification endpoint.
  • Add a blocking lint rule and statistical timing tests for login and reset to CI.

Standards and references

Related NHI Mgmt Group resources: Side-Channel Attacks Guide · Account Enumeration Prevention Guide · Password Hashing and Key Derivation Guide · AES-GCM vs ChaCha20-Poly1305

PDF for subscribers

Take this guide with you

Every section, table and checklist in one PDF, with links to the sources and related guides. Subscribe to our weekly NHI & AI Identity Journal and we’ll email you a download link.

Free weekly newsletter

Subscribe to the NHI & AI Identity Journal

The latest on NHI and Agentic AI security – articles, research, breaches, news and events every week.

Bonus 33% off our NHI Course when you subscribe.

NHIMG Editorial Note
Researched and written by the NHI Mgmt Group editorial team using AI tools, under the editorial direction of Lalit Choda. Facts are drawn from the sources listed on this page and checked by our automated quality process. Last updated 14 November 2026. Spotted an error? Use our contact page.
NHI Mgmt Group, the independent authority on Non-Human Identity, IAM and Agentic AI security. nhimg.org

PDF Downloads for Newsletter Subscribers

Get the guide

Enter your details and we’ll email you a download link.

The full guide is free to read online. The PDF is a thank you for subscribing.

Fill in all fields marked * and tick the PDF box to continue.