fix: use timing-safe comparison for CLI API token validation (#9)

* fix: use timing-safe comparison for CLI API token validation

Replace direct string comparison (===) with constant-time comparison
using crypto.timingSafeEqual to prevent timing attacks that could
leak information about the token character by character.

Affected locations:
- server/src/web/routes/auth.ts (accessToken validation)
- server/src/web/routes/cli.ts (bearer token middleware)
- server/src/socket/server.ts (socket.io /cli namespace auth)

* Update server/src/utils/crypto.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Johnny Wang
2025-12-26 17:22:04 +08:00
committed by GitHub
co-authored by Copilot Claude
parent e2731fc7f0
commit aababe6a57
4 changed files with 20 additions and 3 deletions
+2 -1
View File
@@ -4,6 +4,7 @@ import { jwtVerify } from 'jose'
import { z } from 'zod'
import type { Store } from '../store'
import { configuration } from '../configuration'
import { safeCompareStrings } from '../utils/crypto'
import { registerCliHandlers } from './handlers/cli'
import { registerTerminalHandlers } from './handlers/terminal'
import { RpcRegistry } from './rpcRegistry'
@@ -93,7 +94,7 @@ export function createSocketServer(deps: SocketServerDeps): {
cliNs.use((socket, next) => {
const auth = socket.handshake.auth as Record<string, unknown> | undefined
const token = typeof auth?.token === 'string' ? auth.token : null
if (token !== configuration.cliApiToken) {
if (!safeCompareStrings(token, configuration.cliApiToken)) {
return next(new Error('Invalid token'))
}
next()
+14
View File
@@ -0,0 +1,14 @@
import { timingSafeEqual } from 'node:crypto'
export function safeCompareStrings(a: string | null | undefined, b: string | null | undefined): boolean {
if (a == null || b == null) {
return false
}
const bufA = Buffer.from(a, 'utf8')
const bufB = Buffer.from(b, 'utf8')
try {
return timingSafeEqual(bufA, bufB)
} catch {
return false
}
}
+2 -1
View File
@@ -2,6 +2,7 @@ import { Hono } from 'hono'
import { SignJWT } from 'jose'
import { z } from 'zod'
import { configuration } from '../../configuration'
import { safeCompareStrings } from '../../utils/crypto'
import { validateTelegramInitData } from '../telegramInitData'
import { getOrCreateOwnerId } from '../ownerId'
import type { WebAppEnv } from '../middleware/auth'
@@ -33,7 +34,7 @@ export function createAuthRoutes(jwtSecret: Uint8Array): Hono<WebAppEnv> {
// Access Token authentication (CLI_API_TOKEN)
if ('accessToken' in parsed.data) {
if (parsed.data.accessToken !== configuration.cliApiToken) {
if (!safeCompareStrings(parsed.data.accessToken, configuration.cliApiToken)) {
return c.json({ error: 'Invalid access token' }, 401)
}
userId = await getOrCreateOwnerId()
+2 -1
View File
@@ -1,6 +1,7 @@
import { Hono } from 'hono'
import { z } from 'zod'
import { configuration } from '../../configuration'
import { safeCompareStrings } from '../../utils/crypto'
import type { SyncEngine } from '../../sync/syncEngine'
const bearerSchema = z.string().regex(/^Bearer\s+(.+)$/i)
@@ -32,7 +33,7 @@ export function createCliRoutes(getSyncEngine: () => SyncEngine | null): Hono {
}
const token = parsed.data.replace(/^Bearer\s+/i, '')
if (token !== configuration.cliApiToken) {
if (!safeCompareStrings(token, configuration.cliApiToken)) {
return c.json({ error: 'Invalid token' }, 401)
}