From b55f99de24cdf78bdb4eb54b5ce0fa4d710846e3 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe <259325684+Sucukdeluxe@users.noreply.github.com> Date: Sat, 22 Aug 2026 10:25:32 +0200 Subject: [PATCH] fix: enforce strict online backup keyring entries --- lib/online-backup-keyring.js | 19 ++++++++++++++----- tests/online-backup-keyring.test.js | 15 +++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/lib/online-backup-keyring.js b/lib/online-backup-keyring.js index fc9203d..31ee51f 100644 --- a/lib/online-backup-keyring.js +++ b/lib/online-backup-keyring.js @@ -4,6 +4,8 @@ const crypto = require('node:crypto'); const secretStore = require('./secret-store'); const { parseOnlineBackupKey } = require('./online-backup'); +const STORED_ENTRY_KEYS = ['createdAt', 'encryptedKey', 'id']; + function isObject(value) { return Boolean(value) && typeof value === 'object' && !Array.isArray(value); } @@ -24,8 +26,11 @@ function createOnlineBackupKeyring({ let mutation = Promise.resolve(); function validateEntry(entry) { + if (!isObject(entry)) return null; + const keys = Object.keys(entry).sort(); if ( - !isObject(entry) + keys.length !== STORED_ENTRY_KEYS.length + || !keys.every((key, index) => key === STORED_ENTRY_KEYS[index]) || typeof entry.id !== 'string' || !entry.id || typeof entry.encryptedKey !== 'string' @@ -53,7 +58,7 @@ function createOnlineBackupKeyring({ return { id: entry.id, encryptedKey: entry.encryptedKey, createdAt, key }; } - async function readEntries() { + async function readEntries(rejectInvalidEntries = false) { let contents; try { contents = await fsImpl.readFile(filePath, 'utf8'); @@ -70,7 +75,11 @@ function createOnlineBackupKeyring({ if (!isObject(document) || document.version !== 1 || !Array.isArray(document.entries)) { throw new Error('Gespeicherter Online-Schlüsselbund ist ungültig'); } - return document.entries.map(validateEntry).filter(Boolean); + const entries = document.entries.map(validateEntry); + if (rejectInvalidEntries && entries.some((entry) => !entry)) { + throw new Error('Gespeicherter Online-Schlüsselbund ist ungültig'); + } + return entries.filter(Boolean); } async function writeEntries(entries) { @@ -135,7 +144,7 @@ function createOnlineBackupKeyring({ return serialize(async () => { const validated = validateEntry(entry); if (!validated) throw new Error('Online-Sicherungsschlüssel ist ungültig'); - const entries = await readEntries(); + const entries = await readEntries(true); if (entries.some((current) => current.id === validated.id)) return; await writeEntries([...entries, validated]); }); @@ -143,7 +152,7 @@ function createOnlineBackupKeyring({ function remove(id) { return serialize(async () => { - const entries = await readEntries(); + const entries = await readEntries(true); const remaining = entries.filter((entry) => entry.id !== id); if (remaining.length === entries.length) return false; await writeEntries(remaining); diff --git a/tests/online-backup-keyring.test.js b/tests/online-backup-keyring.test.js index 35b80f8..23a686c 100644 --- a/tests/online-backup-keyring.test.js +++ b/tests/online-backup-keyring.test.js @@ -130,6 +130,21 @@ describe('encrypted online backup keyring', () => { assert.equal(fs.readFileSync(filePath, 'utf8'), '{invalid-json'); }); + it('rejects entries with additional properties without legitimizing the unsafe file during mutation', async () => { + const { filePath, keyring } = fixture(); + const key = validKey(); + const prepared = keyring.prepare(key, '2026-08-22T10:00:00.000Z'); + const contents = JSON.stringify({ + version: 1, + entries: [{ ...prepared, plaintextKey: key }] + }); + fs.writeFileSync(filePath, contents); + + await assert.rejects(keyring.commit(prepared)); + assert.equal(fs.readFileSync(filePath, 'utf8'), contents); + assert.deepEqual(await keyring.list(), []); + }); + it('keeps the previous valid file readable and removes the temporary file after rename fails', async () => { const firstFixture = fixture(); const firstKey = validKey();