mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-08-06 07:00:12 +00:00
fix(security): harden auth and request limits
This commit is contained in:
@@ -42,6 +42,9 @@ function looksLikeEncString(value: string): boolean {
|
||||
*/
|
||||
function validateKdfParams(kdfType: number | undefined, kdfIterations: number | undefined, kdfMemory?: number | undefined, kdfParallelism?: number | undefined): string | null {
|
||||
const type = kdfType ?? 0;
|
||||
if (type !== 0 && type !== 1) {
|
||||
return 'KDF type must be PBKDF2-SHA256 or Argon2id';
|
||||
}
|
||||
if (type === 0) {
|
||||
// PBKDF2-SHA256: minimum 100 000 iterations
|
||||
if (typeof kdfIterations === 'number' && kdfIterations < 100_000) {
|
||||
@@ -448,7 +451,7 @@ export async function handleGetPasswordHint(request: Request, env: Env): Promise
|
||||
}
|
||||
|
||||
const rateLimit = new RateLimitService(env.DB);
|
||||
const minuteBudget = await rateLimit.consumeBudgetWithWindow(
|
||||
const minuteBudget = await rateLimit.consumeStrictBudgetWithWindow(
|
||||
`${clientIdentifier}:password-hint`,
|
||||
LIMITS.rateLimit.passwordHintRequestsPerMinute,
|
||||
60
|
||||
@@ -470,7 +473,7 @@ export async function handleGetPasswordHint(request: Request, env: Env): Promise
|
||||
);
|
||||
}
|
||||
|
||||
const hourlyBudget = await rateLimit.consumeBudgetWithWindow(
|
||||
const hourlyBudget = await rateLimit.consumeStrictBudgetWithWindow(
|
||||
`${clientIdentifier}:password-hint-hour`,
|
||||
LIMITS.rateLimit.passwordHintRequestsPerHour,
|
||||
60 * 60
|
||||
@@ -734,6 +737,11 @@ export async function handleChangePassword(request: Request, env: Env, userId: s
|
||||
const nextKdfParallelism = body.kdfParallelism ?? readNestedNumber(body, ['unlockData', 'kdf', 'parallelism']);
|
||||
const kdfErr = validateKdfParams(nextKdf, nextKdfIterations, nextKdfMemory, nextKdfParallelism);
|
||||
if (kdfErr) return errorResponse(kdfErr, 400);
|
||||
const shouldUpdateHint = typeof body.masterPasswordHint === 'string' || body.masterPasswordHint === null;
|
||||
const nextMasterPasswordHint = shouldUpdateHint ? normalizeMasterPasswordHint(body.masterPasswordHint) : undefined;
|
||||
if (nextMasterPasswordHint && nextMasterPasswordHint.length > 120) {
|
||||
return errorResponse('masterPasswordHint must be 120 characters or fewer', 400);
|
||||
}
|
||||
|
||||
user.masterPasswordHash = await auth.hashPasswordServer(newMasterPasswordHash, user.email);
|
||||
if (nextKey) user.key = nextKey;
|
||||
@@ -743,8 +751,8 @@ export async function handleChangePassword(request: Request, env: Env, userId: s
|
||||
if (typeof nextKdfIterations === 'number') user.kdfIterations = nextKdfIterations;
|
||||
if (typeof nextKdfMemory === 'number') user.kdfMemory = nextKdfMemory;
|
||||
if (typeof nextKdfParallelism === 'number') user.kdfParallelism = nextKdfParallelism;
|
||||
if (typeof body.masterPasswordHint === 'string' || body.masterPasswordHint === null) {
|
||||
user.masterPasswordHint = body.masterPasswordHint;
|
||||
if (shouldUpdateHint) {
|
||||
user.masterPasswordHint = nextMasterPasswordHint ?? null;
|
||||
}
|
||||
user.securityStamp = generateUUID();
|
||||
user.updatedAt = new Date().toISOString();
|
||||
|
||||
@@ -124,6 +124,10 @@ async function processAttachmentUpload(
|
||||
}
|
||||
|
||||
const path = getAttachmentObjectKey(cipherId, attachment.id);
|
||||
if (await getBlobObject(env, path)) {
|
||||
return errorResponse('Attachment file has already been uploaded', 409);
|
||||
}
|
||||
|
||||
try {
|
||||
await putBlobObject(env, path, upload.body, {
|
||||
size: upload.size,
|
||||
|
||||
@@ -5,6 +5,8 @@ import { readAuthRequestDeviceInfo, readActingDeviceIdentifier } from '../utils/
|
||||
import { errorResponse, jsonResponse } from '../utils/response';
|
||||
import { isAuthRequestExpired } from '../services/storage-auth-request-repo';
|
||||
import { notifyAuthRequestResponse, notifyUserAuthRequest } from '../durable/notifications-hub';
|
||||
import { RateLimitService, getClientIdentifier } from '../services/ratelimit';
|
||||
import { LIMITS } from '../config/limits';
|
||||
|
||||
const AUTH_REQUEST_TYPE_AUTHENTICATE_AND_UNLOCK = 0;
|
||||
const AUTH_REQUEST_TYPE_UNLOCK = 1;
|
||||
@@ -131,6 +133,30 @@ async function readJsonBody(request: Request): Promise<Record<string, any> | nul
|
||||
}
|
||||
}
|
||||
|
||||
async function enforceAuthRequestCreateRateLimit(
|
||||
request: Request,
|
||||
env: Env,
|
||||
email: string,
|
||||
deviceIdentifier: string
|
||||
): Promise<Response | null> {
|
||||
const clientIdentifier = getClientIdentifier(request);
|
||||
if (!clientIdentifier) return errorResponse('Client IP is required', 403);
|
||||
|
||||
const rateLimit = new RateLimitService(env.DB);
|
||||
const limit = LIMITS.rateLimit.authRequestRequestsPerMinute;
|
||||
const encodedEmail = encodeURIComponent(email || 'missing');
|
||||
const encodedDevice = encodeURIComponent(deviceIdentifier || 'missing');
|
||||
const budgets = await Promise.all([
|
||||
rateLimit.consumeStrictBudget(`auth-request:ip:${clientIdentifier}`, limit),
|
||||
rateLimit.consumeStrictBudget(`auth-request:email:${encodedEmail}`, limit),
|
||||
rateLimit.consumeStrictBudget(`auth-request:device:${encodedDevice}`, limit),
|
||||
]);
|
||||
const blocked = budgets.find((budget) => !budget.allowed);
|
||||
if (!blocked) return null;
|
||||
|
||||
return errorResponse('Too many authentication requests. Try again later.', 429);
|
||||
}
|
||||
|
||||
function readBodyValue(body: Record<string, any>, names: string[]): unknown {
|
||||
for (const name of names) {
|
||||
if (body[name] !== undefined) return body[name];
|
||||
@@ -164,6 +190,8 @@ export async function handleCreateAuthRequest(request: Request, env: Env): Promi
|
||||
if (!email || !publicKey || !accessCode || !deviceInfo.deviceIdentifier) {
|
||||
return errorResponse('Email, public key, device identifier, and access code are required.', 400);
|
||||
}
|
||||
const rateLimitResponse = await enforceAuthRequestCreateRateLimit(request, env, email, deviceInfo.deviceIdentifier);
|
||||
if (rateLimitResponse) return rateLimitResponse;
|
||||
if (!isSupportedAuthRequestType(type) || type === AUTH_REQUEST_TYPE_ADMIN_APPROVAL) {
|
||||
return errorResponse('Invalid auth request type.', 400);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user