Skip to content

fix(security): harden deployment auth tokens#4760

Open
3em0 wants to merge 1 commit into
simstudioai:mainfrom
3em0:fix-signed-deployment-auth-tokens
Open

fix(security): harden deployment auth tokens#4760
3em0 wants to merge 1 commit into
simstudioai:mainfrom
3em0:fix-signed-deployment-auth-tokens

Conversation

@3em0
Copy link
Copy Markdown

@3em0 3em0 commented May 27, 2026

Summary

  • version deployment auth cookies as signed v2 tokens
  • bind token signatures to the current encrypted deployment password without exposing the truncated password hash in the cookie payload
  • keep signed legacy cookies valid for the existing 24 hour window while rejecting unsigned forged base64 tokens
  • add regression coverage for the forged-token path reported in [BUG] Password-protected deployment auth tokens can be forged #4759

Fixes #4759

Testing

  • git diff --check -- apps/sim/lib/core/security/deployment.ts apps/sim/lib/core/security/deployment.test.ts
  • npx --yes @biomejs/biome@2.0.0-beta.5 check --write --no-errors-on-unmatched --files-ignore-unknown=true apps/sim/lib/core/security/deployment.ts apps/sim/lib/core/security/deployment.test.ts

Not run

  • npx --yes vitest@3.0.8 run apps/sim/lib/core/security/deployment.test.ts --config apps/sim/vitest.config.ts could not start because this checkout has no installed workspace dependencies (@vitejs/plugin-react and @sim/tsconfig/nextjs.json were missing). The expected project runner is Bun, but bun is not installed on this machine and npx bun@1.3.13 fails because npm has postinstall scripts disabled.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped May 27, 2026 1:03pm

Request Review

@cursor
Copy link
Copy Markdown

cursor Bot commented May 27, 2026

PR Summary

High Risk
Changes authentication token format and validation for deployed chat/form access; incorrect handling could lock out users or leave a bypass, though legacy signed cookies remain supported.

Overview
Hardens deployment auth cookies for deployed chat/form endpoints by issuing v2 tokens whose HMAC is bound to the full encrypted deployment password, without embedding the old truncated password hash in the cookie payload.

validateAuthToken now branches on format: v2 tokens verify the new signature and TTL; signed legacy tokens still work for the existing 24-hour window; unsigned base64-forged tokens are rejected. Password rotation invalidates sessions because the signature no longer matches.

Adds deployment.test.ts regression coverage (forged unsigned tokens, tampering, expiry, legacy compatibility).

Reviewed by Cursor Bugbot for commit f9e565b. Bugbot is set up for automated code reviews on this repo. Configure here.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 27, 2026

Greptile Summary

This PR hardens deployment auth cookies by introducing versioned (v2) HMAC-signed tokens that bind the signature to the full SHA-256 of the encrypted password, removing the truncated password hash from the visible cookie payload and closing the unsigned-token forgery path reported in #4759.

  • deployment.ts: Adds AUTH_TOKEN_VERSION = 'v2', a passwordBinding() helper that folds sha256(encryptedPassword) into the HMAC input (not the payload), a hasValidTimestamp() helper, and a dual-path validator that accepts both new v2 tokens and legacy signed tokens for the existing 24-hour window.
  • deployment.test.ts: New test file covering forge rejection, password-change invalidation, payload tampering, expiry, and legacy compatibility; test helpers correctly replicate the production HMAC scheme using UTF-8 encoding, but the suite was not executed in CI (noted in the PR description).

Confidence Score: 3/5

The auth hardening logic is sound and the HMAC construction is correct, but a gap in hasValidTimestamp means forward-dated tokens pass expiry validation without restriction, and the test suite has not been run against the actual source.

The dual-path validator correctly routes v2 and legacy tokens, password binding is wired into the HMAC input rather than the cookie payload, and the test helpers faithfully mirror the production HMAC scheme. However, hasValidTimestamp returns true whenever Date.now() - createdAt is negative — any token carrying a future timestamp bypasses the 24-hour window. Non-browser callers or manually set cookies are unprotected by the browser maxAge bound.

apps/sim/lib/core/security/deployment.ts — the hasValidTimestamp function; apps/sim/lib/core/security/deployment.test.ts — confirm the suite passes in CI before merging.

Important Files Changed

Filename Overview
apps/sim/lib/core/security/deployment.ts Introduces v2 signed tokens that bind the HMAC to the full sha256 of the encrypted password (instead of exposing a truncated hash in the payload), keeps a backward-compatible legacy validation path, and adds hasValidTimestamp — but that helper accepts forward-dated timestamps, so tokens with createdAt in the future are never rejected.
apps/sim/lib/core/security/deployment.test.ts New test file covering the core security properties (forge rejection, password invalidation, tampering, expiry, legacy compatibility); helpers correctly mirror production UTF-8 HMAC logic, but tests were not executed and the future-timestamp edge case is untested.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Incoming token cookie] --> B[base64 decode]
    B --> C[split on lastColon: payload + sig]
    C --> D{parts0 === v2?}

    D -- Yes-v2 --> E{parts.length === 4?}
    E -- No --> REJECT1[return false]
    E -- Yes --> F[HMAC over payload + sha256-encryptedPassword]
    F --> G{sig matches?}
    G -- No --> REJECT2[return false]
    G -- Yes --> H{storedId === deploymentId?}
    H -- No --> REJECT3[return false]
    H -- Yes --> I{hasValidTimestamp?}
    I -- No --> REJECT4[return false]
    I -- Yes --> ACCEPT1[return true]

    D -- No-legacy --> J{parts.length === 4?}
    J -- No --> REJECT5[return false]
    J -- Yes --> K[HMAC over payload only]
    K --> L{sig matches?}
    L -- No --> REJECT6[return false]
    L -- Pass --> M{storedId === deploymentId?}
    M -- No --> REJECT7[return false]
    M -- Yes --> N{pwSlot matches?}
    N -- No --> REJECT8[return false]
    N -- Yes --> O{hasValidTimestamp?}
    O -- No --> REJECT9[return false]
    O -- Yes --> ACCEPT2[return true]

    subgraph note [hasValidTimestamp - future timestamps accepted]
        I
        O
    end
Loading

Reviews (1): Last reviewed commit: "fix(security): harden deployment auth to..." | Re-trigger Greptile

Comment on lines +45 to +50
function hasValidTimestamp(timestamp: string): boolean {
const createdAt = Number.parseInt(timestamp, 10)
if (!Number.isFinite(createdAt)) return false

return Date.now() - createdAt <= AUTH_TOKEN_TTL_MS
}
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.

P1 hasValidTimestamp accepts tokens with a future createdAt. When createdAt > Date.now(), the expression Date.now() - createdAt is negative, which always satisfies <= AUTH_TOKEN_TTL_MS (86 400 000), so the check returns true unconditionally. In a browser context the maxAge cookie attribute enforces the wall-clock bound, but any server-side use, non-browser client, or manually constructed cookie bypasses that. Adding a lower bound rejects forward-dated timestamps regardless of context.

Suggested change
function hasValidTimestamp(timestamp: string): boolean {
const createdAt = Number.parseInt(timestamp, 10)
if (!Number.isFinite(createdAt)) return false
return Date.now() - createdAt <= AUTH_TOKEN_TTL_MS
}
function hasValidTimestamp(timestamp: string): boolean {
const createdAt = Number.parseInt(timestamp, 10)
if (!Number.isFinite(createdAt)) return false
const elapsed = Date.now() - createdAt
return elapsed >= 0 && elapsed <= AUTH_TOKEN_TTL_MS
}

Comment on lines +108 to +113
it('rejects expired signed tokens', () => {
const expiredTimestamp = Date.now() - 24 * 60 * 60 * 1000 - 1
const token = signedV2Token('dep_test', 'encrypted-password', expiredTimestamp)

expect(validateAuthToken(token, 'dep_test', 'encrypted-password')).toBe(false)
})
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.

P2 No coverage for the future-timestamp path

The expiry suite tests a timestamp 1 ms past 24 h but has no counterpart asserting that a token with timestamp = Date.now() + someOffset is rejected. Given the <= boundary issue in hasValidTimestamp, a test like signedV2Token('dep_test', 'encrypted-password', Date.now() + 60_000) would currently pass validation (returns true) rather than being rejected, confirming the bug in the implementation.

Comment on lines +1 to +16
import { createHash, createHmac } from 'node:crypto'
import { createEnvMock } from '@sim/testing'
import type { NextResponse } from 'next/server'
import { describe, expect, it, vi } from 'vitest'

vi.mock('@/lib/core/config/env', () =>
createEnvMock({
BETTER_AUTH_SECRET: 'deployment-auth-test-secret-32-chars',
})
)

vi.mock('@/lib/core/config/feature-flags', () => ({
isDev: true,
}))

import { setDeploymentAuthCookie, validateAuthToken } from './deployment'
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.

P2 Tests not executed — confirmed by PR description

The PR explicitly notes npx vitest run … could not be started due to missing workspace dependencies. The test helpers (signedLegacyToken, signedV2Token) faithfully replicate the production HMAC logic (UTF-8 encoding matches sha256Hex/hmacSha256Hex in @sim/security), so the tests are structurally correct. However, for a security-hardening change the lack of a green CI run is a gap worth closing before merge, especially for the new rejects expired signed tokens and rejects tampered signed token payloads cases.

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.

[BUG] Password-protected deployment auth tokens can be forged

1 participant