fix(security): bound untrusted wire decoding

req: security/005
This commit is contained in:
slhx agent
2026-07-13 21:31:34 +02:00
parent 2a8aa4c07f
commit 3ac9549cc3
6 changed files with 335 additions and 24 deletions
+37 -1
View File
@@ -117,12 +117,48 @@ async function openLog() {
return requestResult(request);
}
export function validateQueuedCommand(command) {
if (!command || Object.getPrototypeOf(command) !== Object.prototype) {
throw new TypeError("queued command must be an object");
}
if (command.schemaVersion !== COMMAND_SCHEMA) {
throw new RangeError(`unsupported queued command schema version ${command.schemaVersion}`);
}
const boundedString = (field, maximum) => {
const value = command[field];
if (typeof value !== "string" || value.length === 0 || value.length > maximum) {
throw new TypeError(`queued command ${field} is invalid`);
}
};
boundedString("id", 256);
boundedString("accountPartition", 128);
boundedString("actor", 128);
boundedString("session", 128);
boundedString("cardId", 128);
if (!Number.isSafeInteger(command.causal) || command.causal < 1) {
throw new TypeError("queued command causal is invalid");
}
const queuedAt = command.queuedAt === undefined ? 0 : command.queuedAt;
if (!Number.isSafeInteger(queuedAt) || queuedAt < 0) {
throw new TypeError("queued command queuedAt is invalid");
}
if (command.kind !== "reorder_card") throw new TypeError(`unknown queued command kind ${command.kind}`);
if (command.targetColumn !== "done") throw new TypeError(`unknown queued command target ${command.targetColumn}`);
if (!["click", "drop", "keydown"].includes(command.eventKind)) {
throw new TypeError(`unknown queued command event kind ${command.eventKind}`);
}
if (command.key !== null && (typeof command.key !== "string" || command.key.length > 64)) {
throw new TypeError("queued command key is invalid");
}
return command.queuedAt === undefined ? { ...command, queuedAt } : command;
}
async function pendingCommands(database) {
const transaction = database.transaction(COMMANDS, "readonly");
const done = transactionDone(transaction);
const commands = await requestResult(transaction.objectStore(COMMANDS).index(ACCOUNT_INDEX).getAll(accountPartition));
await done;
return commands.sort((left, right) => left.causal - right.causal);
return commands.map(validateQueuedCommand).sort((left, right) => left.causal - right.causal);
}
async function removePendingCommand(database, commandId) {