From a287a8cf126d53d7ada5ce9c82f2d1ff2cc6d488 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 6 Feb 2026 11:18:54 +0000 Subject: [PATCH 1/2] Fix timing attack length leak in examples Co-authored-by: elithrar --- .../protect-against-timing-attacks.mdx | 65 ++++++++++--------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/src/content/docs/workers/examples/protect-against-timing-attacks.mdx b/src/content/docs/workers/examples/protect-against-timing-attacks.mdx index 54c3b97233a..c582325904f 100644 --- a/src/content/docs/workers/examples/protect-against-timing-attacks.mdx +++ b/src/content/docs/workers/examples/protect-against-timing-attacks.mdx @@ -32,6 +32,10 @@ The `timingSafeEqual` function takes two `ArrayBuffer` or `TypedArray` values to Note that this function is not constant time with respect to the length of the parameters and also does not guarantee constant time for the surrounding code. Handling of secrets should be taken with care to not introduce timing side channels. +:::caution +Do not return early when the input and secret have different lengths. An early return leaks the length of the secret through response timing. Instead, always perform a constant-time comparison as shown in the examples below — when lengths differ, compare the user input against itself and negate the result so the check still fails but takes the same amount of time. +::: + In order to compare two strings, you must use the [`TextEncoder`](/workers/runtime-apis/encoding/#textencoder) API. @@ -49,20 +53,21 @@ export default { const authToken = req.headers.get("Authorization") || ""; - if (authToken.length !== env.MY_SECRET_VALUE.length) { - return new Response("Unauthorized", { status: 401 }); - } - const encoder = new TextEncoder(); - const a = encoder.encode(authToken); - const b = encoder.encode(env.MY_SECRET_VALUE); + const userValue = encoder.encode(authToken); + const secretValue = encoder.encode(env.MY_SECRET_VALUE); - if (a.byteLength !== b.byteLength) { - return new Response("Unauthorized", { status: 401 }); - } + // Do not return early when lengths differ — that leaks the secret's + // length through timing. Instead, always perform a constant-time + // comparison: when the lengths match compare directly; otherwise + // compare the user input against itself (always true) and negate. + const lengthsMatch = userValue.byteLength === secretValue.byteLength; + const isEqual = lengthsMatch + ? crypto.subtle.timingSafeEqual(userValue, secretValue) + : !crypto.subtle.timingSafeEqual(userValue, userValue); - if (!crypto.subtle.timingSafeEqual(a, b)) { + if (!isEqual) { return new Response("Unauthorized", { status: 401 }); } @@ -85,17 +90,18 @@ class Default(WorkerEntrypoint): if secret is None: return Response("Missing secret binding", status=500) - if len(auth_token) != len(secret): - return Response("Unauthorized", status=401) - encoder = TextEncoder.new() - a = encoder.encode(auth_token) - b = encoder.encode(secret) + user_value = encoder.encode(auth_token) + secret_value = encoder.encode(secret) - if a.byteLength != b.byteLength: - return Response("Unauthorized", status=401) + # Do not return early when lengths differ — that leaks the secret's + # length through timing. Always perform a constant-time comparison. + if user_value.byteLength == secret_value.byteLength: + is_equal = crypto.subtle.timingSafeEqual(user_value, secret_value) + else: + is_equal = not crypto.subtle.timingSafeEqual(user_value, user_value) - if not crypto.subtle.timingSafeEqual(a, b): + if not is_equal: return Response("Unauthorized", status=401) return Response("Welcome!") @@ -124,22 +130,21 @@ app.use('*', async (c, next) => { const authToken = c.req.header("Authorization") || ""; - // Early length check to avoid unnecessary processing - if (authToken.length !== secret.length) { - return c.text("Unauthorized", 401); - } - const encoder = new TextEncoder(); - const a = encoder.encode(authToken); - const b = encoder.encode(secret); + const userValue = encoder.encode(authToken); + const secretValue = encoder.encode(secret); - if (a.byteLength !== b.byteLength) { - return c.text("Unauthorized", 401); - } + // Do not return early when lengths differ — that leaks the secret's + // length through timing. Instead, always perform a constant-time + // comparison: when the lengths match compare directly; otherwise + // compare the user input against itself (always true) and negate. + const lengthsMatch = userValue.byteLength === secretValue.byteLength; + const isEqual = lengthsMatch + ? crypto.subtle.timingSafeEqual(userValue, secretValue) + : !crypto.subtle.timingSafeEqual(userValue, userValue); - // Perform timing-safe comparison - if (!crypto.subtle.timingSafeEqual(a, b)) { + if (!isEqual) { return c.text("Unauthorized", 401); } From 792962554c150b54aed0ff867191fbb6b495e474 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 6 Feb 2026 11:33:36 +0000 Subject: [PATCH 2/2] Fix timing attack length leak in basic-auth.mdx examples Replace early 'return false' on length mismatch with 'return !timingSafeEqual(aBytes, aBytes)' so a constant-time comparison always runs, preventing an attacker from deducing the secret length through response timing. --- src/content/docs/workers/examples/basic-auth.mdx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/content/docs/workers/examples/basic-auth.mdx b/src/content/docs/workers/examples/basic-auth.mdx index 0632e141499..ea0a980f176 100644 --- a/src/content/docs/workers/examples/basic-auth.mdx +++ b/src/content/docs/workers/examples/basic-auth.mdx @@ -55,10 +55,10 @@ function timingSafeEqual(a, b) { const aBytes = encoder.encode(a); const bBytes = encoder.encode(b); + // Do not return early when lengths differ — that leaks the secret's + // length through timing. Compare against self and negate instead. if (aBytes.byteLength !== bBytes.byteLength) { - // Strings must be the same length in order to compare - // with crypto.subtle.timingSafeEqual - return false; + return !crypto.subtle.timingSafeEqual(aBytes, aBytes); } return crypto.subtle.timingSafeEqual(aBytes, bBytes); @@ -169,10 +169,10 @@ function timingSafeEqual(a: string, b: string) { const aBytes = encoder.encode(a); const bBytes = encoder.encode(b); + // Do not return early when lengths differ — that leaks the secret's + // length through timing. Compare against self and negate instead. if (aBytes.byteLength !== bBytes.byteLength) { - // Strings must be the same length in order to compare - // with crypto.subtle.timingSafeEqual - return false; + return !crypto.subtle.timingSafeEqual(aBytes, aBytes); } return crypto.subtle.timingSafeEqual(aBytes, bBytes);