fix(security): bound untrusted wire decoding
req: security/005
This commit is contained in:
@@ -226,6 +226,81 @@ async fn flat_patch_persists_offline_then_uploads_with_same_operation_identity(
|
||||
"const root = document.querySelector('[data-hemx-root]'); return root.hasAttribute('data-hemx-client-ready') && root.hasAttribute('data-hemx-sync-ready')",
|
||||
)
|
||||
.await?;
|
||||
let rejected_inputs = driver
|
||||
.execute_async(
|
||||
r#"
|
||||
const done = arguments[arguments.length - 1];
|
||||
(async () => {
|
||||
const { reorder_card: handler } = await import('/kanban_client.js');
|
||||
const errors = {};
|
||||
for (const [name, args] of [
|
||||
['unknown-version', [99, 'click', null, null, null, 1, '1|2']],
|
||||
['oversized-kind', [1, 'x'.repeat(257), null, null, null, 1, '1|2']],
|
||||
['oversized-value', [1, 'click', 'x'.repeat(64 * 1024 + 1), null, null, 1, '1|2']],
|
||||
['oversized-key', [1, 'keydown', null, null, 'x'.repeat(1025), 1, '1|2']],
|
||||
['unknown-state-version', [1, 'click', null, null, null, 99, '1|2']],
|
||||
['oversized-state', [1, 'click', null, null, null, 1, 'x'.repeat(1024 * 1024 + 1)]],
|
||||
]) {
|
||||
try {
|
||||
await handler(...args);
|
||||
errors[name] = null;
|
||||
} catch (error) {
|
||||
errors[name] = String(error);
|
||||
}
|
||||
}
|
||||
done(errors);
|
||||
})().catch((error) => done({ harness: String(error) }));
|
||||
"#,
|
||||
Vec::new(),
|
||||
)
|
||||
.await?
|
||||
.json()
|
||||
.clone();
|
||||
assert!(
|
||||
rejected_inputs
|
||||
.as_object()
|
||||
.expect("client rejection record")
|
||||
.values()
|
||||
.all(|error| error.as_str().is_some_and(|message| !message.is_empty())),
|
||||
"malformed client-local input reached the handler: {rejected_inputs}"
|
||||
); // req: security/005
|
||||
let rejected_sync = driver
|
||||
.execute_async(
|
||||
r#"
|
||||
const done = arguments[arguments.length - 1];
|
||||
(async () => {
|
||||
const root = document.querySelector('[data-hemx-root]');
|
||||
const before = document.querySelector('#kanban-status').textContent;
|
||||
const errors = {};
|
||||
for (const [name, detail] of [
|
||||
['malformed', '{'],
|
||||
['unknown-version', JSON.stringify({ schemaVersion: 99, idempotencyKey: 'event', operationId: 'event', key: 'cardColumn', value: 'done' })],
|
||||
['invalid-key', JSON.stringify({ schemaVersion: 1, idempotencyKey: 'event', operationId: 'event', key: 'value', value: 'done' })],
|
||||
['oversized-value', JSON.stringify({ schemaVersion: 1, idempotencyKey: 'event', operationId: 'event', key: 'cardColumn', value: 'x'.repeat(4097) })],
|
||||
]) {
|
||||
root.removeAttribute('data-hemx-sync-error');
|
||||
document.dispatchEvent(new CustomEvent('hemx:sync-patch', { detail }));
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
errors[name] = root.getAttribute('data-hemx-sync-error');
|
||||
}
|
||||
done({ errors, pending: root.getAttribute('data-hemx-sync-pending'), unchanged: before === document.querySelector('#kanban-status').textContent });
|
||||
})().catch((error) => done({ harness: String(error) }));
|
||||
"#,
|
||||
Vec::new(),
|
||||
)
|
||||
.await?
|
||||
.json()
|
||||
.clone();
|
||||
assert!(
|
||||
rejected_sync["errors"]
|
||||
.as_object()
|
||||
.expect("sync rejection record")
|
||||
.values()
|
||||
.all(|error| error.as_str().is_some_and(|message| !message.is_empty())),
|
||||
"malformed sync patch reached durable storage: {rejected_sync}"
|
||||
);
|
||||
assert_eq!(rejected_sync["pending"], "0", "{rejected_sync}");
|
||||
assert_eq!(rejected_sync["unchanged"], true, "{rejected_sync}");
|
||||
driver
|
||||
.execute(
|
||||
"window.__clientErrors = []; document.querySelector('[data-hemx-root]').addEventListener('hemx:client-error', event => window.__clientErrors.push(event.detail)); Object.defineProperty(Navigator.prototype, 'onLine', { configurable: true, get: () => false }); document.querySelector('[data-hemx-client-event=drop]').dispatchEvent(new Event('drop', { bubbles: true })); return true",
|
||||
|
||||
Reference in New Issue
Block a user