mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-09-19 11:10:12 +00:00
fix(cipher): clear omitted notes on full update (#363)
Treat omitted nullable cipher fields as cleared during full updates so stale encrypted notes are not restored by merge fallback. Fixes #362
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
interface AliasedValue<T> {
|
||||
present: boolean;
|
||||
value: T | null | undefined;
|
||||
}
|
||||
|
||||
function readOwnAliasedValue<T>(source: unknown, aliases: readonly string[]): AliasedValue<T> {
|
||||
if (!source || typeof source !== 'object') {
|
||||
return { present: false, value: undefined };
|
||||
}
|
||||
|
||||
const record = source as Record<string, unknown>;
|
||||
for (const alias of aliases) {
|
||||
if (Object.prototype.hasOwnProperty.call(record, alias)) {
|
||||
return { present: true, value: record[alias] as T | null | undefined };
|
||||
}
|
||||
}
|
||||
|
||||
return { present: false, value: undefined };
|
||||
}
|
||||
|
||||
/**
|
||||
* Full cipher updates use replacement semantics for nullable fields.
|
||||
* Bitwarden clients may omit a property after its value is cleared, so an
|
||||
* absent property must become null instead of falling back to stored data.
|
||||
*/
|
||||
export function readNullableFullUpdateField<T>(
|
||||
source: unknown,
|
||||
aliases: readonly string[]
|
||||
): T | null {
|
||||
const incoming = readOwnAliasedValue<T>(source, aliases);
|
||||
return incoming.present ? incoming.value ?? null : null;
|
||||
}
|
||||
+5
-10
@@ -27,6 +27,7 @@ import { deleteAllAttachmentsForCipher, deleteAllAttachmentsForCiphers } from '.
|
||||
import { parsePagination, encodeContinuationToken } from '../utils/pagination';
|
||||
import { readActingDeviceIdentifier } from '../utils/device';
|
||||
import { auditRequestMetadata, writeAuditEvent } from '../services/audit-events';
|
||||
import { readNullableFullUpdateField } from './cipher-full-update';
|
||||
|
||||
// CONTRACT:
|
||||
// Cipher JSON is the highest-risk Bitwarden compatibility surface. Preserve
|
||||
@@ -1100,16 +1101,10 @@ export async function handleUpdateCipher(request: Request, env: Env, userId: str
|
||||
cipher.passwordHistory = incomingPasswordHistory.value ?? null;
|
||||
}
|
||||
|
||||
// Custom fields deletion compatibility:
|
||||
// - Accept both camelCase "fields" and PascalCase "Fields".
|
||||
// - For full update (PUT/POST on this endpoint), missing fields means cleared fields.
|
||||
// This prevents stale custom fields from being resurrected by merge fallback.
|
||||
const incomingFields = getAliasedProp(cipherData, ['fields', 'Fields']);
|
||||
if (incomingFields.present) {
|
||||
cipher.fields = incomingFields.value ?? null;
|
||||
} else if (request.method === 'PUT' || request.method === 'POST') {
|
||||
cipher.fields = null;
|
||||
}
|
||||
// Nullable fields use replacement semantics on this full-update endpoint.
|
||||
// Some clients omit cleared values, so merge fallback must not resurrect them.
|
||||
cipher.notes = readNullableFullUpdateField<string>(cipherData, ['notes', 'Notes']);
|
||||
cipher.fields = readNullableFullUpdateField<Cipher['fields']>(cipherData, ['fields', 'Fields']);
|
||||
normalizeCipherForStorage(cipher);
|
||||
const compatibilityError = validateCipherEncryptedFieldsForCompatibility(cipher);
|
||||
if (compatibilityError) return errorResponse(compatibilityError, 400);
|
||||
|
||||
Reference in New Issue
Block a user