From 60dd298dee8a9cea47f0562be2ae5488efbba3b7 Mon Sep 17 00:00:00 2001 From: shuaiplus <2327005759@qq.com> Date: Thu, 2 Jul 2026 17:20:51 +0800 Subject: [PATCH] fix(security): harden jwt config and password rotation --- .dev.vars.example | 5 ----- src/handlers/accounts.ts | 9 +++------ src/handlers/attachments.ts | 10 ++++------ src/handlers/sends-public.ts | 8 +++----- src/handlers/sends-shared.ts | 4 ++-- src/router-public.ts | 7 ++----- src/router.ts | 24 +++++++++++++++++------- src/types/index.ts | 4 ---- src/utils/direct-upload.ts | 4 ++-- webapp/src/components/JwtWarningPage.tsx | 6 ++---- webapp/src/lib/api/auth.ts | 5 ++++- webapp/src/lib/app-auth.ts | 2 +- webapp/src/lib/i18n/locales/en.ts | 2 +- webapp/src/lib/i18n/locales/es.ts | 2 +- webapp/src/lib/i18n/locales/ru.ts | 2 +- webapp/src/lib/i18n/locales/zh-CN.ts | 2 +- webapp/src/lib/i18n/locales/zh-TW.ts | 2 +- webapp/src/lib/types.ts | 2 +- 18 files changed, 46 insertions(+), 54 deletions(-) delete mode 100644 .dev.vars.example diff --git a/.dev.vars.example b/.dev.vars.example deleted file mode 100644 index df2fedb..0000000 --- a/.dev.vars.example +++ /dev/null @@ -1,5 +0,0 @@ -# JWT Secret for signing tokens (required) -# IMPORTANT: change this value before any real deployment. -# Generate one with: openssl rand -hex 32 -# (Example only, 64 hex chars = 32 bytes) -JWT_SECRET=Enter-your-JWT-key-here-at-least-32-characters diff --git a/src/handlers/accounts.ts b/src/handlers/accounts.ts index 9512609..443122c 100644 --- a/src/handlers/accounts.ts +++ b/src/handlers/accounts.ts @@ -1,4 +1,4 @@ -import { Env, User, DEFAULT_DEV_SECRET } from '../types'; +import { Env, User } from '../types'; import { StorageService } from '../services/storage'; import { AuthService } from '../services/auth'; import { RateLimitService, getClientIdentifier } from '../services/ratelimit'; @@ -150,10 +150,9 @@ function normalizeMasterPasswordHint(input: string | null | undefined): string | return normalized ? normalized : null; } -function jwtSecretUnsafeReason(env: Env): 'missing' | 'default' | 'too_short' | null { +function jwtSecretUnsafeReason(env: Env): 'missing' | 'too_short' | null { const secret = (env.JWT_SECRET || '').trim(); if (!secret) return 'missing'; - if (secret === DEFAULT_DEV_SECRET) return 'default'; if (secret.length < LIMITS.auth.jwtSecretMinLength) return 'too_short'; return null; } @@ -242,9 +241,7 @@ export async function handleRegister(request: Request, env: Env): Promise { - const secret = (env.JWT_SECRET || '').trim(); - if (!secret || secret.length < LIMITS.auth.jwtSecretMinLength || secret === DEFAULT_DEV_SECRET) { - return errorResponse('Server configuration error', 500); - } + const secret = getSafeJwtSecret(env); + if (!secret) return errorResponse('Server configuration error', 500); const url = new URL(request.url); const token = url.searchParams.get('token'); @@ -418,7 +416,7 @@ export async function handlePublicDownloadAttachment( } // Verify token - const claims = await verifyFileDownloadToken(token, env.JWT_SECRET); + const claims = await verifyFileDownloadToken(token, secret); if (!claims) { return errorResponse('Invalid or expired token', 401); } diff --git a/src/handlers/sends-public.ts b/src/handlers/sends-public.ts index ae11321..1b171d0 100644 --- a/src/handlers/sends-public.ts +++ b/src/handlers/sends-public.ts @@ -3,7 +3,6 @@ import { StorageService } from '../services/storage'; import { RateLimitService, getClientIdentifier } from '../services/ratelimit'; import { jsonResponse, errorResponse } from '../utils/response'; import { sanitizeDownloadContentType } from '../utils/content-type'; -import { LIMITS } from '../config/limits'; import { createSendAccessToken, createSendFileDownloadToken, @@ -113,10 +112,9 @@ export async function handleAccessSendFile( idOrAccessId: string, fileId: string ): Promise { - const secret = (env.JWT_SECRET || '').trim(); - if (!secret || secret.length < LIMITS.auth.jwtSecretMinLength) { - return errorResponse('Server configuration error', 500); - } + const safeSecret = getSafeJwtSecret(env); + if (!safeSecret.ok) return safeSecret.response; + const { secret } = safeSecret; const storage = new StorageService(env.DB); const send = await resolveSendFromIdOrAccessId(storage, idOrAccessId); diff --git a/src/handlers/sends-shared.ts b/src/handlers/sends-shared.ts index b513422..866fd49 100644 --- a/src/handlers/sends-shared.ts +++ b/src/handlers/sends-shared.ts @@ -1,4 +1,4 @@ -import { Env, Send, SendAuthType, SendResponse, SendType, DEFAULT_DEV_SECRET } from '../types'; +import { Env, Send, SendAuthType, SendResponse, SendType } from '../types'; import { notifyUserSendCreate, notifyUserSendDelete, @@ -371,7 +371,7 @@ export function hasEmailAuth(send: Send): boolean { export function getSafeJwtSecret(env: Env): { ok: true; secret: string } | { ok: false; response: Response } { const secret = (env.JWT_SECRET || '').trim(); - if (!secret || secret.length < LIMITS.auth.jwtSecretMinLength || secret === DEFAULT_DEV_SECRET) { + if (!secret || secret.length < LIMITS.auth.jwtSecretMinLength) { return { ok: false, response: errorResponse('Server configuration error', 500) }; } return { ok: true, secret }; diff --git a/src/router-public.ts b/src/router-public.ts index b22c0c9..9644245 100644 --- a/src/router-public.ts +++ b/src/router-public.ts @@ -1,5 +1,4 @@ import { LIMITS } from './config/limits'; -import { DEFAULT_DEV_SECRET } from './types'; import { handleAccessSend, handleAccessSendFile, @@ -34,7 +33,7 @@ import { StorageService } from './services/storage'; import type { Env } from './types'; type PublicRateLimiter = (category?: string, maxRequests?: number) => Promise; -type JwtUnsafeReason = 'missing' | 'default' | 'too_short' | null; +type JwtUnsafeReason = 'missing' | 'too_short' | null; export interface WebBootstrapResponse { defaultKdfIterations: number; @@ -308,9 +307,7 @@ export async function buildWebBootstrapResponse(env: Env): Promise = { "txt_jwt_secret_value_label": "Value:", "txt_jwt_secret_value_requirement": "Random string with at least {min} characters", "txt_jwt_what_is": "What is JWT?", - "txt_jwt_what_is_body": "JWT_SECRET is the server-side signing key used to issue and verify login tokens. If it is missing, too short, or still using the sample value, the instance is not safe to use normally.", + "txt_jwt_what_is_body": "JWT_SECRET is the server-side signing key used to issue and verify login tokens. If it is missing or too short, the instance is not safe to use normally.", "txt_how_to_fix": "How to fix", "txt_jwt_fix_step_1": "Open your deployment environment variables.", "txt_jwt_fix_step_2": "If your current key is not random enough, use the 32-character generator below.", diff --git a/webapp/src/lib/i18n/locales/es.ts b/webapp/src/lib/i18n/locales/es.ts index 14f412a..dc89273 100644 --- a/webapp/src/lib/i18n/locales/es.ts +++ b/webapp/src/lib/i18n/locales/es.ts @@ -669,7 +669,7 @@ const es: Record = { "txt_jwt_secret_value_label": "Valor:", "txt_jwt_secret_value_requirement": "Cadena aleatoria de al menos {min} caracteres", "txt_jwt_what_is": "Qué es JWT", - "txt_jwt_what_is_body": "JWT_SECRET es la clave de firma del lado del servidor utilizada para emitir y verificar tokens de inicio de sesión. Si no está presente, es demasiado corta o todavía usa el valor de ejemplo, la instancia no es segura para uso normal.", + "txt_jwt_what_is_body": "JWT_SECRET es la clave de firma del lado del servidor utilizada para emitir y verificar tokens de inicio de sesión. Si no está presente o es demasiado corta, la instancia no es segura para uso normal.", "txt_how_to_fix": "Cómo corregirlo", "txt_jwt_fix_step_1": "Abra las variables de entorno de su despliegue.", "txt_jwt_fix_step_2": "Si su clave actual no es lo suficientemente aleatoria, use el generador de 32 caracteres a continuación.", diff --git a/webapp/src/lib/i18n/locales/ru.ts b/webapp/src/lib/i18n/locales/ru.ts index fc46598..8817624 100644 --- a/webapp/src/lib/i18n/locales/ru.ts +++ b/webapp/src/lib/i18n/locales/ru.ts @@ -669,7 +669,7 @@ const ru: Record = { "txt_jwt_secret_value_label": "Значение:", "txt_jwt_secret_value_requirement": "Случайная строка, содержащая не менее {min} символов.", "txt_jwt_what_is": "Что такое JWT?", - "txt_jwt_what_is_body": "JWT_SECRET — это ключ подписи на стороне сервера, используемый для выдачи и проверки токенов входа. Если он отсутствует, слишком короткий или все еще использует образец значения, обычное использование экземпляра небезопасно.", + "txt_jwt_what_is_body": "JWT_SECRET — это ключ подписи на стороне сервера, используемый для выдачи и проверки токенов входа. Если он отсутствует или слишком короткий, обычное использование экземпляра небезопасно.", "txt_how_to_fix": "Как исправить", "txt_jwt_fix_step_1": "Откройте переменные среды развертывания.", "txt_jwt_fix_step_2": "Если ваш текущий ключ недостаточно случайный, используйте 32-значный генератор ниже.", diff --git a/webapp/src/lib/i18n/locales/zh-CN.ts b/webapp/src/lib/i18n/locales/zh-CN.ts index 9e9ba04..3b0f3a0 100644 --- a/webapp/src/lib/i18n/locales/zh-CN.ts +++ b/webapp/src/lib/i18n/locales/zh-CN.ts @@ -669,7 +669,7 @@ const zhCN: Record = { "txt_jwt_secret_value_label": "值:", "txt_jwt_secret_value_requirement": "最低 {min} 位随机字符", "txt_jwt_what_is": "JWT 是什么", - "txt_jwt_what_is_body": "JWT_SECRET 是服务端用来签发和校验登录令牌的密钥。如果它缺失、过短,或者仍然使用示例值,实例就不能安全地正常使用。", + "txt_jwt_what_is_body": "JWT_SECRET 是服务端用来签发和校验登录令牌的密钥。如果它缺失或过短,实例就不能安全地正常使用。", "txt_how_to_fix": "处理步骤(添加 / 更换)", "txt_jwt_fix_step_1": "你可以继续下一步,不影响使用。", "txt_jwt_fix_step_2": "如果当前密钥不是强随机值,建议使用下方 32 位生成器。", diff --git a/webapp/src/lib/i18n/locales/zh-TW.ts b/webapp/src/lib/i18n/locales/zh-TW.ts index da2b31f..563a969 100644 --- a/webapp/src/lib/i18n/locales/zh-TW.ts +++ b/webapp/src/lib/i18n/locales/zh-TW.ts @@ -669,7 +669,7 @@ const zhTW: Record = { "txt_jwt_secret_value_label": "值:", "txt_jwt_secret_value_requirement": "最低 {min} 位隨機字符", "txt_jwt_what_is": "JWT 是什麼", - "txt_jwt_what_is_body": "JWT_SECRET 是服務端用來簽發和校驗登錄令牌的密鑰。如果它缺失、過短,或者仍然使用示例值,實例就不能安全地正常使用。", + "txt_jwt_what_is_body": "JWT_SECRET 是服務端用來簽發和校驗登錄令牌的密鑰。如果它缺失或過短,實例就不能安全地正常使用。", "txt_how_to_fix": "處理步驟(添加 / 更換)", "txt_jwt_fix_step_1": "你可以繼續下一步,不影響使用。", "txt_jwt_fix_step_2": "如果當前密鑰不是強隨機值,建議使用下方 32 位生成器。", diff --git a/webapp/src/lib/types.ts b/webapp/src/lib/types.ts index c19413a..41d9bda 100644 --- a/webapp/src/lib/types.ts +++ b/webapp/src/lib/types.ts @@ -290,7 +290,7 @@ export interface ListResponse { export interface WebBootstrapResponse { defaultKdfIterations?: number; - jwtUnsafeReason?: 'missing' | 'default' | 'too_short' | null; + jwtUnsafeReason?: 'missing' | 'too_short' | null; jwtSecretMinLength?: number; registrationInviteRequired?: boolean; }