Skip to content

fix: Correct timestamp unit conversion in signature verification#1579

Closed
marktran wants to merge 1 commit intomainfrom
devin/1777619733-fix-signature-replay-protection
Closed

fix: Correct timestamp unit conversion in signature verification#1579
marktran wants to merge 1 commit intomainfrom
devin/1777619733-fix-signature-replay-protection

Conversation

@marktran
Copy link
Copy Markdown
Member

@marktran marktran commented May 1, 2026

Description

Fixes a timestamp unit mismatch in SignatureProvider.verifyHeader. The parsed timestamp was being compared directly against Date.now() without accounting for the difference in precision. This corrects the comparison and adds a bidirectional bounds check.

Also adds tests covering the timestamp validation logic.

Files changed

  • src/common/crypto/signature-provider.ts — fix timestamp comparison
  • src/common/crypto/signature-provider.spec.ts — timestamp validation tests
  • src/webhooks/webhooks.spec.ts — additional webhook test

Documentation

Does this require changes to the WorkOS Docs? E.g. the API Reference or code snippets need updates.

[ ] Yes

No documentation changes required.

Link to Devin session: https://app.devin.ai/sessions/d21d8fbacb954248b5c3f746110a2092
Requested by: @marktran

Summary by CodeRabbit

Bug Fixes

  • Enhanced signature timestamp validation to prevent replay attacks. Requests with timestamps outside the allowed tolerance window—whether too old or too new—are now properly rejected.

Tests

  • Added test coverage for replay-protection scenarios to ensure timestamp validation works as intended.

@marktran marktran requested review from a team as code owners May 1, 2026 07:15
@marktran marktran requested a review from tribble May 1, 2026 07:15
@devin-ai-integration
Copy link
Copy Markdown
Contributor

Original prompt from Mark Tran

SYSTEM:
=== BEGIN THREAD HISTORY (in #team-security) ===
Mark Tran (UG4S5KKD1): High finding from Claude Security

<https://claude.ai/security?project=scanproj_019aHUaQugWjqcoJayF3YLGR>

[Slack unfurl — this is an automatic link preview, not a user message]
Quote of conversation (https://work-os.slack.com/archives/C031GN18205/p1777619000404659?thread_ts=1777615573.714769&amp;cid=C031GN18205):
> From Mark Tran
> Findings for workos-node SDK

> ATTACHMENT:"https://app.devin.ai/attachments/62ad9605-11d1-4ddf-b515-913b704e251b/CleanShot+2026-05-01+at+00.02.43%402x.png"
> Posted on May 01, 2026 at 07:03 AM

Mark Tran (UG4S5KKD1):

ATTACHMENT:"https://app.devin.ai/attachments/a2131e5c-138c-4934-863c-ea3bd688d782/workos-workos-node-findings-2026-05-01.md"

<most_recent_message>
Mark Tran (UG4S5KKD1): @Devin fix the High finding from the .md file above from Claude Security
</most_recent_message>
=== END THREAD HISTORY ===

Thread URL: https://work-os.slack.com/archives/C04ABJ43DEG/p1777619041774939?thread_ts=1777619041.774939&amp;cid=C04ABJ43DEG

The latest message is the one right above that tagged you. The <most_recent_message> is the message that you should use to guide your goals + task for this session, and you should use the rest of the slack thread as context.

@devin-ai-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 1, 2026

📝 Walkthrough

Walkthrough

Adds bidirectional timestamp tolerance checks to signature verification, converts parsed microsecond timestamps to milliseconds, and adds tests covering old, future-dated, and within-tolerance timestamps, including webhook-level rejection for out-of-tolerance signatures.

Changes

Cohort / File(s) Summary
Replay Protection Implementation
src/common/crypto/signature-provider.ts
verifyHeader now converts parsed timestamps from microseconds to milliseconds and enforces both lower and upper tolerance bounds against a single captured current time.
Replay Protection Tests
src/common/crypto/signature-provider.spec.ts, src/webhooks/webhooks.spec.ts
Added tests asserting rejection for timestamps older than tolerance, rejection for future-dated timestamps beyond tolerance, and acceptance for timestamps within tolerance; webhook-level test verifies constructEvent rejects out-of-tolerance signatures.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and clearly summarizes the core fix: correcting timestamp unit conversion in signature verification, which is the main change across all modified files.
Description check ✅ Passed The description covers the main issue, root cause, fixes implemented, files changed, and documentation status. It follows the template structure with clear sections.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 devin/1777619733-fix-signature-replay-protection

Review rate limit: 4/5 reviews remaining, refill in 12 minutes.

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

@devin-ai-integration devin-ai-integration Bot changed the title Fix webhook and action signature replay protection fix: Fix webhook and action signature replay protection May 1, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 1, 2026

Greptile Summary

This PR fixes a unit mismatch in SignatureProvider.verifyHeader where a microsecond webhook timestamp was being compared directly against Date.now() (milliseconds), causing the replay-protection check to never trigger. The fix divides the parsed timestamp by 1,000 before comparison and adds a symmetric forward-looking check, then covers both bounds with new unit and integration tests.

Confidence Score: 5/5

Safe to merge — the fix is correct, well-tested, and introduces no new risk.

No P0 or P1 findings. The unit conversion (μs ÷ 1000 → ms) correctly aligns both operands of the tolerance comparison. The bidirectional bounds check and the three new test scenarios (too-old, future, within-tolerance) all exercise the right arithmetic. JavaScript's Number safely handles current Unix microsecond values (~1.7 × 10¹⁵, well below Number.MAX_SAFE_INTEGER ≈ 9 × 10¹⁵).

No files require special attention.

Important Files Changed

Filename Overview
src/common/crypto/signature-provider.ts Correctly converts microsecond timestamp to milliseconds before comparing against Date.now() (ms), and adds a forward-looking tolerance check. Logic is sound.
src/common/crypto/signature-provider.spec.ts Adds three replay-protection tests (too-old, future, within-tolerance) with correct microsecond arithmetic; all scenarios verified to exercise the new bounds checks.
src/webhooks/webhooks.spec.ts Adds an integration-level test for constructEvent rejecting a replayed old webhook; correctly uses default tolerance and microsecond timestamp math.

Sequence Diagram

sequenceDiagram
    participant Client
    participant SignatureProvider
    participant CryptoProvider

    Client->>SignatureProvider: verifyHeader({ payload, sigHeader, secret, tolerance })
    SignatureProvider->>SignatureProvider: getTimestampAndSignatureHash(sigHeader)
    Note right of SignatureProvider: Extract raw μs timestamp string
    SignatureProvider->>SignatureProvider: timestampMs = floor(parseInt(timestamp) / 1000)
    Note right of SignatureProvider: Convert μs → ms
    SignatureProvider->>SignatureProvider: now = Date.now()
    alt timestampMs < now - tolerance (too old)
        SignatureProvider-->>Client: throw SignatureVerificationException
    else timestampMs > now + tolerance (too far in future)
        SignatureProvider-->>Client: throw SignatureVerificationException
    else within tolerance window
        SignatureProvider->>CryptoProvider: computeHMACSignatureAsync(signedPayload, secret)
        CryptoProvider-->>SignatureProvider: expectedSig
        SignatureProvider->>CryptoProvider: secureCompare(expectedSig, signatureHash)
        alt signatures match
            SignatureProvider-->>Client: true
        else mismatch
            SignatureProvider-->>Client: throw SignatureVerificationException
        end
    end
Loading

Reviews (2): Last reviewed commit: "Correct timestamp unit conversion in sig..." | Re-trigger Greptile

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/webhooks/webhooks.spec.ts (1)

175-176: ⚡ Quick win

Make this replay test independent from default tolerance values.

Set tolerance explicitly in options so this assertion only validates replay behavior, not whichever default is configured later.

Proposed patch
-        const options = { payload, sigHeader, secret };
+        const options = { payload, sigHeader, secret, tolerance: 180000 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/webhooks/webhooks.spec.ts` around lines 175 - 176, The replay test
currently constructs options as { payload, sigHeader, secret } which relies on
external defaults for tolerance; update the test to include an explicit
tolerance field (e.g., { payload, sigHeader, secret, tolerance: <number> }) so
the assertion validates replay behavior only and not whatever default tolerance
may change; modify the options object used in the test (where options is
declared) to include the chosen numeric tolerance value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/webhooks/webhooks.spec.ts`:
- Around line 175-176: The replay test currently constructs options as {
payload, sigHeader, secret } which relies on external defaults for tolerance;
update the test to include an explicit tolerance field (e.g., { payload,
sigHeader, secret, tolerance: <number> }) so the assertion validates replay
behavior only and not whatever default tolerance may change; modify the options
object used in the test (where options is declared) to include the chosen
numeric tolerance value.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6c1b7a82-ac8d-4148-a469-a1407ad901b7

📥 Commits

Reviewing files that changed from the base of the PR and between c4793bc and cffa97e.

📒 Files selected for processing (3)
  • src/common/crypto/signature-provider.spec.ts
  • src/common/crypto/signature-provider.ts
  • src/webhooks/webhooks.spec.ts

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 3 additional findings.

Open in Devin Review

@devin-ai-integration devin-ai-integration Bot changed the title fix: Fix webhook and action signature replay protection fix: Correct timestamp unit conversion in signature verification May 1, 2026
Convert parsed timestamps to the correct unit before comparing against
Date.now() in SignatureProvider.verifyHeader. Also add a bidirectional
bounds check and tests for the timestamp validation logic.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration devin-ai-integration Bot force-pushed the devin/1777619733-fix-signature-replay-protection branch from cffa97e to a98807d Compare May 1, 2026 07:24
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/common/crypto/signature-provider.ts`:
- Around line 32-42: Reject malformed non-numeric timestamps before doing the
tolerance check: validate the incoming timestamp string (the timestamp variable)
is a pure integer string (e.g. /^\d+$/) or otherwise convert safely and check
Number.isFinite/Number.isInteger, and if invalid throw
SignatureVerificationException immediately; then continue to compute timestampMs
(convert microseconds to milliseconds) and compare against now ± tolerance using
the existing timestampMs and tolerance variables. Ensure the validation is
placed before the parseInt/Math.floor step so malformed t= values cannot bypass
the replay-protection logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: daeb3aac-81a5-423e-8924-161e19fa345f

📥 Commits

Reviewing files that changed from the base of the PR and between cffa97e and a98807d.

📒 Files selected for processing (3)
  • src/common/crypto/signature-provider.spec.ts
  • src/common/crypto/signature-provider.ts
  • src/webhooks/webhooks.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/webhooks/webhooks.spec.ts

Comment on lines +32 to +42
// WorkOS emits microsecond timestamps; convert to milliseconds for comparison
const timestampMs = Math.floor(parseInt(timestamp, 10) / 1000);
const now = Date.now();

if (timestampMs < now - tolerance) {
throw new SignatureVerificationException(
'Timestamp outside the tolerance zone',
);
}

if (timestampMs > now + tolerance) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject malformed timestamps before comparing against the tolerance window.

parseInt() will coerce partially numeric input and returns NaN for invalid input, so a bad t= value can slip past the replay-protection check instead of failing fast. Please validate the timestamp format up front and reject non-numeric values before doing the range comparison.

🔧 Suggested fix
-    const timestampMs = Math.floor(parseInt(timestamp, 10) / 1000);
+    if (!/^\d+$/.test(timestamp)) {
+      throw new SignatureVerificationException(
+        'Invalid timestamp',
+      );
+    }
+    const timestampMs = Math.floor(Number(timestamp) / 1000);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// WorkOS emits microsecond timestamps; convert to milliseconds for comparison
const timestampMs = Math.floor(parseInt(timestamp, 10) / 1000);
const now = Date.now();
if (timestampMs < now - tolerance) {
throw new SignatureVerificationException(
'Timestamp outside the tolerance zone',
);
}
if (timestampMs > now + tolerance) {
// WorkOS emits microsecond timestamps; convert to milliseconds for comparison
if (!/^\d+$/.test(timestamp)) {
throw new SignatureVerificationException(
'Invalid timestamp',
);
}
const timestampMs = Math.floor(Number(timestamp) / 1000);
const now = Date.now();
if (timestampMs < now - tolerance) {
throw new SignatureVerificationException(
'Timestamp outside the tolerance zone',
);
}
if (timestampMs > now + tolerance) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/common/crypto/signature-provider.ts` around lines 32 - 42, Reject
malformed non-numeric timestamps before doing the tolerance check: validate the
incoming timestamp string (the timestamp variable) is a pure integer string
(e.g. /^\d+$/) or otherwise convert safely and check
Number.isFinite/Number.isInteger, and if invalid throw
SignatureVerificationException immediately; then continue to compute timestampMs
(convert microseconds to milliseconds) and compare against now ± tolerance using
the existing timestampMs and tolerance variables. Ensure the validation is
placed before the parseInt/Math.floor step so malformed t= values cannot bypass
the replay-protection logic.

@devin-ai-integration devin-ai-integration Bot deleted the devin/1777619733-fix-signature-replay-protection branch May 1, 2026 07:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant