Mark the guarded publication arguments as checked, or the audit throws.

The guard replaced `check(boardId, String)` with `Match.test(...)`, and every
subscription then failed with

  Exception from sub board id ... Error: Did not check() all arguments during
  publisher 'board'

This app runs with audit-argument-checks: a publisher that returns without having
check()ed every one of its arguments is failed by the audit, and `Match.test` is
not checking - it only asks. So the server no longer died, but no board could be
subscribed to either.

Each guarded publisher now marks its arguments with `check(x, Match.Any)` - which
satisfies the audit and cannot throw - and validates them with `Match.test`
immediately after. `board`, `boardCardsWindow` and `boardCardsLoadingMode` all do
this. Checked the other publications too: `activities` was the only other one
that looked unchecked, and it does check its argument, over several lines.

tests/publicationArgumentGuard.test.cjs now requires both halves - every argument
marked with Match.Any, and the marking before the validation - so removing the
check() again fails the suite instead of the server.

Thanks to xet7 !
This commit is contained in:
Lauri Ojansivu committed 2026-07-26 23:30:15 +03:00
1 parent 66be7313f3
commit b3537985eb
3 files changed
+32

No files matched your search

+9
View File
@@ -489,6 +489,15 @@ publishComposite('board', async function(boardId, isArchived) {
// Publishing nothing is the right answer to a subscription that names no
// board: publishComposite treats a falsy return as "no publications" and
// readies the subscription, so the client simply gets an empty result.
//
// `check(..., Match.Any)` first: this app runs with audit-argument-checks, which
// fails a publisher that returns without having check()ed every argument -
// "Did not check() all arguments during publisher 'board'". Match.test alone
// does not count as checking, so the audit threw for every subscription. Any
// marks them audited and never throws; the real validation is the two lines
// below it.
check(boardId, Match.Any);
check(isArchived, Match.Any);
if (!Match.test(boardId, String) || !boardId) return;
if (!Match.test(isArchived, Boolean)) return;
+8
View File
@@ -42,6 +42,12 @@ async function boardVisibleTo(userId, boardId) {
publishComposite('boardCardsWindow', function(boardId, cardSelector, sort, limit) {
// Same as the `board` publication: a bad argument from a client must end the
// subscription, not the server process. A falsy return publishes nothing.
// `Match.Any` marks each argument as checked for audit-argument-checks (which
// fails a publisher that did not check them all); the validation is below.
check(boardId, Match.Any);
check(cardSelector, Match.Any);
check(sort, Match.Any);
check(limit, Match.Any);
if (!Match.test(boardId, String) || !boardId) return;
if (!Match.test(cardSelector, Object)) return;
if (!Match.test(sort, Match.OneOf(Object, null, undefined))) return;
@@ -184,6 +190,8 @@ Meteor.publish('boardListCardCount', async function(countId, boardId, cardSelect
// is rare and picked up on the next board open). #6480.
Meteor.publish('boardCardsLoadingMode', async function(boardId) {
// A null board id here is the same crash: this publisher is async too.
// Match.Any marks it checked for audit-argument-checks; the test is the guard.
check(boardId, Match.Any);
if (!Match.test(boardId, String) || !boardId) return this.ready();
const board = await boardVisibleTo(this.userId, boardId);
+15
View File
@@ -61,6 +61,16 @@ test('the board publisher tests its arguments before anything else', () => {
assert.ok(/if \(!Match\.test\(isArchived, Boolean\)\) return;/.test(head));
assert.ok(!/^\s*check\(boardId, String\);/m.test(head),
'check() throws, and a throw here exits the process - it must not be the guard');
// ...but the arguments must still be MARKED as checked: this app runs with
// audit-argument-checks, which fails any publisher that returns without having
// check()ed every argument ("Did not check() all arguments during publisher
// 'board'"). Match.test does not count; check(x, Match.Any) does, and never
// throws.
assert.ok(/check\(boardId, Match\.Any\);/.test(head)
&& /check\(isArchived, Match\.Any\);/.test(head),
'every argument must be check()ed with Match.Any, or the audit throws');
assert.ok(head.indexOf('check(boardId, Match.Any)') < head.indexOf('Match.test(boardId'),
'marked first, validated second');
// The guard has to come before any work, or the work throws first.
assert.ok(head.indexOf('Match.test(boardId') < head.indexOf('localizeBoardMemberAvatars'),
'and it must run before the publisher does anything with the id');
@@ -71,7 +81,12 @@ test('the card-window publishers are guarded the same way', () => {
assert.ok(/if \(!Match\.test\(boardId, String\) \|\| !boardId\) return;/.test(window.slice(0, 900)));
assert.ok(/if \(!Match\.test\(cardSelector, Object\)\) return;/.test(window.slice(0, 900)),
'a card selector from a client is an object or nothing at all');
for (const arg of ['boardId', 'cardSelector', 'sort', 'limit']) {
assert.ok(new RegExp(`check\\(${arg}, Match\\.Any\\);`).test(window.slice(0, 900)),
`${arg} must be marked as checked for the audit`);
}
const mode = cardsWindow.slice(cardsWindow.indexOf("Meteor.publish('boardCardsLoadingMode'"));
assert.ok(/check\(boardId, Match\.Any\);/.test(mode.slice(0, 500)));
assert.ok(/if \(!Match\.test\(boardId, String\) \|\| !boardId\) return this\.ready\(\);/
.test(mode.slice(0, 500)),
'a plain publisher readies the subscription instead of returning a config');