diff --git a/src/handlers/ciphers.ts b/src/handlers/ciphers.ts index 8a4bc27..73a8653 100644 --- a/src/handlers/ciphers.ts +++ b/src/handlers/ciphers.ts @@ -32,6 +32,7 @@ import { auditRequestMetadata, writeAuditEvent } from '../services/audit-events' // attachments, import/export, and current official clients. export interface CipherResponseOptions { preserveRepairableUris?: boolean; + validFolderIds?: ReadonlySet; } export function shouldPreserveRepairableCipherUris(request: Request): boolean { @@ -48,6 +49,12 @@ function normalizeOptionalId(value: unknown): string | null { return normalized ? normalized : null; } +function normalizeResponseFolderId(folderId: unknown, validFolderIds?: ReadonlySet): string | null { + const normalized = normalizeOptionalId(folderId); + if (!normalized) return null; + return validFolderIds && !validFolderIds.has(normalized) ? null : normalized; +} + function readBooleanOrFallback(value: unknown, fallback: boolean): boolean { return typeof value === 'boolean' ? value : fallback; } @@ -727,7 +734,7 @@ export function cipherToResponse( // Pass through ALL stored cipher fields (known + unknown) ...passthrough, // Server-computed / enforced fields (always override) - folderId: normalizeOptionalId(cipher.folderId), + folderId: normalizeResponseFolderId(cipher.folderId, options.validFolderIds), type: Number(cipher.type) || 1, organizationId: normalizeOptionalId((passthrough as any).organizationId ?? null), organizationUseTotp: !!((passthrough as any).organizationUseTotp ?? false), @@ -785,9 +792,10 @@ export async function handleGetCiphers(request: Request, env: Env, userId: strin const attachmentsByCipher = await storage.getAttachmentsByCipherIds( filteredCiphers.map((cipher) => cipher.id) ); + const validFolderIds = new Set((await storage.getAllFolders(userId)).map((folder) => folder.id)); // Build responses only for the current page to keep pagination cheap. - const responseOptions = cipherResponseOptionsForRequest(request); + const responseOptions = { ...cipherResponseOptionsForRequest(request), validFolderIds }; const cipherResponses: CipherResponse[] = []; for (const cipher of filteredCiphers) { const attachments = attachmentsByCipher.get(cipher.id) || []; diff --git a/src/handlers/sync.ts b/src/handlers/sync.ts index 1ea0125..dbbca5b 100644 --- a/src/handlers/sync.ts +++ b/src/handlers/sync.ts @@ -88,12 +88,13 @@ export async function handleSync(request: Request, env: Env, userId: string): Pr .map(buildWebAuthnPrfOption) .filter((option): option is NonNullable => !!option); const userDecryptionOptions = buildUserDecryptionOptions(user, webAuthnPrfOptions[0] || null); + const validFolderIds = new Set(folders.map((folder) => folder.id)); const profile: ProfileResponse = buildProfileResponse(user, env); const cipherResponses: CipherResponse[] = []; for (const cipher of ciphers) { - const response = cipherToResponse(cipher, attachmentsByCipher.get(cipher.id) || [], { preserveRepairableUris }); + const response = cipherToResponse(cipher, attachmentsByCipher.get(cipher.id) || [], { preserveRepairableUris, validFolderIds }); if (isCipherResponseSyncCompatible(response)) { cipherResponses.push(response); } diff --git a/src/services/storage-folder-repo.ts b/src/services/storage-folder-repo.ts index 9e1701a..02c7660 100644 --- a/src/services/storage-folder-repo.ts +++ b/src/services/storage-folder-repo.ts @@ -44,9 +44,14 @@ export async function clearFolderFromCiphers( `UPDATE ciphers SET folder_id = NULL, updated_at = ?, data = json_remove(data, '$.folderId', '$.folder_id', '$.updatedAt', '$.revisionDate') - WHERE user_id = ? AND folder_id = ?` + WHERE user_id = ? + AND ( + folder_id = ? + OR json_extract(data, '$.folderId') = ? + OR json_extract(data, '$.folder_id') = ? + )` ) - .bind(now, userId, folderId) + .bind(now, userId, folderId, folderId, folderId) .run(); } @@ -71,9 +76,14 @@ export async function bulkDeleteFolders( `UPDATE ciphers SET folder_id = NULL, updated_at = ?, data = json_remove(data, '$.folderId', '$.folder_id', '$.updatedAt', '$.revisionDate') - WHERE user_id = ? AND folder_id IN (${placeholders})` + WHERE user_id = ? + AND ( + folder_id IN (${placeholders}) + OR json_extract(data, '$.folderId') IN (${placeholders}) + OR json_extract(data, '$.folder_id') IN (${placeholders}) + )` ) - .bind(now, userId, ...chunk) + .bind(now, userId, ...chunk, ...chunk, ...chunk) .run(); await db