Skip to content

🛡️ Sentinel: [CRITICAL] Fix string comparison timing leak in authentication checks#59

Open
aloewright wants to merge 1 commit into
mainfrom
sentinel/timing-safe-equal-fix-17101550073275665915
Open

🛡️ Sentinel: [CRITICAL] Fix string comparison timing leak in authentication checks#59
aloewright wants to merge 1 commit into
mainfrom
sentinel/timing-safe-equal-fix-17101550073275665915

Conversation

@aloewright

@aloewright aloewright commented Jun 1, 2026

Copy link
Copy Markdown
Owner

🚨 Severity: CRITICAL
💡 Vulnerability: The application was using timingSafeEqual from hono/utils/buffer directly with JavaScript strings to compare authentication secrets (admin keys and webhook signatures). Direct string comparison in crypto functions can cause timing leaks due to internal JS engine string optimizations (e.g. flat strings vs ropes, UTF-16 encoding differences).
🎯 Impact: An attacker could potentially extract the ADMIN_API_KEY or bypass webhook signature verification by measuring the time differences during the timingSafeEqual evaluation, leading to unauthenticated access or forged polar webhook events.
🔧 Fix: Refactored the comparisons to convert the expected and provided strings to Uint8Array buffers using new TextEncoder().encode() before passing them to timingSafeEqual. This guarantees that the check operates securely on constant-sized byte arrays.
Verification: Verified that linting, formatting and type checking pass. Tested that timingSafeEqual works properly with encoded Uint8Array buffers.


PR created automatically by Jules for task 17101550073275665915 started by @aloewright

Summary by CodeRabbit

  • Bug Fixes

    • Strengthened webhook signature verification to use constant-time byte comparisons, reducing timing-attack vulnerability.
    • Improved admin authentication validation with constant-time comparison of encoded credentials and immediate rejection for missing keys.
  • Documentation

    • Added guidance on implementing secure constant-time comparisons with proper byte encoding.

…cation checks

Co-authored-by: aloewright <3641844+aloewright@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 1, 2026 05:29
@google-labs-jules

Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Security hardening applies constant-time byte-array comparisons to webhook signature verification and admin authentication using TextEncoder and timingSafeEqual, preventing timing-attack leakage through raw string comparison. Changes documented in sentinel knowledge base.

Changes

Timing-attack constant-time comparison fixes

Layer / File(s) Summary
Timing-attack constant-time comparison pattern documentation
.jules/sentinel.md
Sentinel entry documents that timingSafeEqual must encode inputs as Uint8Array via TextEncoder rather than comparing raw strings directly, avoiding engine string handling issues and timing-attack leakage.
Webhook signature verification timing-attack fix
apps/quill/src/lib/polar.ts
Webhook v1 signature matching encodes both expected and candidate signatures using TextEncoder before running timingSafeEqual on the resulting byte buffers for constant-time comparison.
Admin key authentication timing-attack fix
apps/quill/src/routes/admin.ts
Admin key middleware separates missing header handling with dedicated 401 response, and encodes both expected and provided keys using TextEncoder before timingSafeEqual comparison on byte arrays.

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and specifically addresses the main change: fixing timing leak vulnerabilities in authentication checks by refactoring string comparisons to use proper byte encoding with timingSafeEqual.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sentinel/timing-safe-equal-fix-17101550073275665915

Warning

Review ran into problems

🔥 Problems

These MCP integrations need to be re-authenticated in the Integrations settings: Sentry


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates webhook verification and admin key validation to convert string secrets to Uint8Array buffers using TextEncoder before comparing them with timingSafeEqual, addressing potential timing side-channel attacks. The review feedback suggests hashing the admin keys with SHA-256 before comparison to prevent length-based timing leaks and runtime exceptions, and checking buffer lengths before calling timingSafeEqual in the webhook verification to avoid potential crashes.

Comment on lines +25 to 29
// Convert strings to Uint8Array before comparison to prevent timing leaks
const enc = new TextEncoder();
if (!(await timingSafeEqual(enc.encode(expected), enc.encode(provided)))) {
throw new HTTPException(401, { message: "Invalid admin key." });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

Comparing the ADMIN_API_KEY directly using timingSafeEqual when the lengths of the expected and provided keys differ can leak the length of the secret key (since timingSafeEqual returns early or behaves differently on length mismatch). In some environments (like Node.js), it can even throw a TypeError, causing an unhandled 500 Internal Server Error.

To prevent both the length timing leak and any potential runtime exceptions, hash both keys using SHA-256 before comparing them. This ensures both inputs to timingSafeEqual are always exactly 32 bytes.

Suggested change
// Convert strings to Uint8Array before comparison to prevent timing leaks
const enc = new TextEncoder();
if (!(await timingSafeEqual(enc.encode(expected), enc.encode(provided)))) {
throw new HTTPException(401, { message: "Invalid admin key." });
}
const enc = new TextEncoder();
const expectedHash = await crypto.subtle.digest("SHA-256", enc.encode(expected));
const providedHash = await crypto.subtle.digest("SHA-256", enc.encode(provided));
if (!(await timingSafeEqual(expectedHash, providedHash))) {
throw new HTTPException(401, { message: "Invalid admin key." });
}

Comment on lines +185 to 197
const enc = new TextEncoder();
const expectedBuf = enc.encode(expected);

for (const c of candidates) {
const [version, sig] = c.split(",");
if (version === "v1" && sig && (await timingSafeEqual(sig, expected))) {
if (
version === "v1" &&
sig &&
(await timingSafeEqual(enc.encode(sig), expectedBuf))
) {
return true;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

Passing buffers of different lengths to timingSafeEqual can cause runtime exceptions (such as TypeError: Inputs must have the same length in Node.js/hybrid environments) or result in early returns. Since the expected signature length is public knowledge (44 bytes for base64-encoded SHA-256), we can safely check that the lengths match before calling timingSafeEqual. This prevents runtime crashes and avoids redundant encoding when the signature is invalid or missing.

  const enc = new TextEncoder();
  const expectedBuf = enc.encode(expected);

  for (const c of candidates) {
    const [version, sig] = c.split(",");
    if (version === "v1" && sig) {
      const sigBuf = enc.encode(sig);
      if (sigBuf.byteLength === expectedBuf.byteLength && (await timingSafeEqual(sigBuf, expectedBuf))) {
        return true;
      }
    }
  }

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
postpilot a2b6e22 Jun 01 2026, 05:31 AM

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.jules/sentinel.md (1)

4-4: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing newline causes malformed markdown heading.

Line 4 concatenates the end of the previous entry with the next section header (...tokens.## 2024-05-19). A blank line is needed before ## 2024-05-19 to render the heading correctly.

Proposed fix
-**Prevention:** Replace direct equality operators with timingSafeEqual for authentication headers or tokens.## 2024-05-19 - Timing Safe Equal for Webhooks
+**Prevention:** Replace direct equality operators with timingSafeEqual for authentication headers or tokens.
+
+## 2024-05-19 - Timing Safe Equal for Webhooks
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.jules/sentinel.md at line 4, The markdown heading "## 2024-05-19 - Timing
Safe Equal for Webhooks" is concatenated to the previous sentence ("tokens.")
causing a malformed heading; insert a single blank line between the end of the
paragraph that mentions "Replace direct equality operators with timingSafeEqual
for authentication headers or tokens." and the heading "## 2024-05-19 - Timing
Safe Equal for Webhooks" so the heading renders correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In @.jules/sentinel.md:
- Line 4: The markdown heading "## 2024-05-19 - Timing Safe Equal for Webhooks"
is concatenated to the previous sentence ("tokens.") causing a malformed
heading; insert a single blank line between the end of the paragraph that
mentions "Replace direct equality operators with timingSafeEqual for
authentication headers or tokens." and the heading "## 2024-05-19 - Timing Safe
Equal for Webhooks" so the heading renders correctly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 329ded20-ff86-4725-aa77-119a30f90b2e

📥 Commits

Reviewing files that changed from the base of the PR and between bab8f67 and a2b6e22.

📒 Files selected for processing (3)
  • .jules/sentinel.md
  • apps/quill/src/lib/polar.ts
  • apps/quill/src/routes/admin.ts

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens authentication-related secret comparisons in the Quill app by ensuring timingSafeEqual operates on encoded byte arrays rather than raw JavaScript strings, reducing the risk of timing side-channel leaks in admin key and webhook signature verification.

Changes:

  • Updated admin x-admin-key check to encode strings to Uint8Array before timingSafeEqual.
  • Updated Polar webhook signature verification to compare encoded Uint8Array buffers instead of strings.
  • Documented the finding and prevention guidance in Sentinel notes.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
apps/quill/src/routes/admin.ts Encodes admin key strings to bytes before constant-time comparison.
apps/quill/src/lib/polar.ts Encodes webhook signatures/expected values to bytes before constant-time comparison.
.jules/sentinel.md Adds Sentinel entry documenting the issue and remediation guidance.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +25 to +27
// Convert strings to Uint8Array before comparison to prevent timing leaks
const enc = new TextEncoder();
if (!(await timingSafeEqual(enc.encode(expected), enc.encode(provided)))) {
Comment thread .jules/sentinel.md
**Learning:** JS engines (like V8) use JIT compilation and can optimize loops or short-circuit operations in ways that defeat the constant-time execution of custom code, leaving the application vulnerable to timing attacks.
**Prevention:** In Cloudflare Workers/Hono, always use the asynchronous `timingSafeEqual` from `hono/utils/buffer` (ensure it is awaited) for comparing strings or buffers that represent secrets to prevent timing side-channel attacks.
## 2025-02-24 - Timing safe equal using Hono buffer
**Vulnerability:** timingSafeEqual in apps/quill/src/lib/polar.ts and apps/quill/src/routes/admin.ts passed strings directly. Passing string variables could lead to engine string storage issues or leaking timing info and throwing error directly.
@aloewright aloewright enabled auto-merge (squash) June 4, 2026 05:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants