diff --git a/server/src/socket/server.ts b/server/src/socket/server.ts index d9f13381..13ec9c2d 100644 --- a/server/src/socket/server.ts +++ b/server/src/socket/server.ts @@ -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 | 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() diff --git a/server/src/utils/crypto.ts b/server/src/utils/crypto.ts new file mode 100644 index 00000000..e5a015ea --- /dev/null +++ b/server/src/utils/crypto.ts @@ -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 + } +} diff --git a/server/src/web/routes/auth.ts b/server/src/web/routes/auth.ts index c5700cf4..cff16cb7 100644 --- a/server/src/web/routes/auth.ts +++ b/server/src/web/routes/auth.ts @@ -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 { // 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() diff --git a/server/src/web/routes/cli.ts b/server/src/web/routes/cli.ts index 7b230889..fa15be5d 100644 --- a/server/src/web/routes/cli.ts +++ b/server/src/web/routes/cli.ts @@ -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) }