Skip to content

Commit 1aefafa

Browse files
JohnMcLearclaude
andcommitted
fix: reject - ops with foreign author to prevent pool injection
The '-' op attribs are discarded from the document but still get added to the pad's attribute pool by moveOpsToNewPool. Without this check, an attacker could inject a fabricated author ID into the pool via a '-' op, then use a '=' op to attribute text to that fabricated author (bypassing the pool existence check). Now all non-'=' ops (+, -) with foreign author IDs are rejected. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 81457c5 commit 1aefafa

File tree

2 files changed

+41
-5
lines changed

2 files changed

+41
-5
lines changed

src/node/handler/PadMessageHandler.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -669,11 +669,6 @@ const handleUserChanges = async (socket:any, message: {
669669
// an attribute number isn't in the pool).
670670
const opAuthorId = AttributeMap.fromString(op.attribs, wireApool).get('author');
671671
if (opAuthorId && opAuthorId !== thisSession.author) {
672-
if (op.opcode === '+') {
673-
// Inserting new text as another author is always impersonation.
674-
throw new Error(`Author ${thisSession.author} tried to submit changes as author ` +
675-
`${opAuthorId} in changeset ${changeset}`);
676-
}
677672
if (op.opcode === '=') {
678673
// Allow restoring author attributes on existing text (undo of clear authorship),
679674
// but only if the author ID is already known to this pad. This prevents a user
@@ -683,6 +678,14 @@ const handleUserChanges = async (socket:any, message: {
683678
throw new Error(`Author ${thisSession.author} tried to set unknown author ` +
684679
`${opAuthorId} on existing text in changeset ${changeset}`);
685680
}
681+
} else {
682+
// Reject '+' ops (inserting new text as another author) and '-' ops (deleting
683+
// with another author's attribs). While '-' attribs are discarded from the
684+
// document, they are added to the pad's attribute pool by moveOpsToNewPool,
685+
// which could be exploited to inject fabricated author IDs into the pool and
686+
// bypass the '=' op pool check above.
687+
throw new Error(`Author ${thisSession.author} tried to submit changes as author ` +
688+
`${opAuthorId} in changeset ${changeset}`);
686689
}
687690
}
688691
}

src/tests/backend/specs/undo_clear_authorship.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,5 +300,38 @@ describe(__filename, function () {
300300
assert.deepEqual(msg, {disconnect: 'badChangeset'},
301301
'Should reject = op with fabricated author not in pad pool');
302302
});
303+
304+
it('should reject - op with foreign author to prevent pool injection', async function () {
305+
// Security: a '-' op with a foreign author's attribs should be rejected.
306+
// While '-' attribs are discarded from the document, they are added to the
307+
// pad's attribute pool by moveOpsToNewPool. Without this check, an attacker
308+
// could inject a fabricated author ID into the pool via a '-' op, then use
309+
// a '=' op to attribute text to that fabricated author.
310+
const userA = await connectUser();
311+
socketA = userA.socket;
312+
revA = userA.rev;
313+
314+
// User A types text
315+
const apoolA = new AttributePool();
316+
apoolA.putAttrib(['author', userA.author]);
317+
await Promise.all([
318+
waitForAcceptCommit(socketA, revA + 1),
319+
sendUserChanges(socketA, revA, 'Z:1>5*0+5$hello', apoolA),
320+
]);
321+
revA += 1;
322+
323+
// User A tries to delete a char with a fabricated author attrib via a - op
324+
// This would inject the fabricated author into the pad pool
325+
const fakePool = new AttributePool();
326+
fakePool.putAttrib(['author', 'a.fabricatedAuthorId']);
327+
328+
const resultP = waitForCommitOrDisconnect(socketA);
329+
// Delete 1 char with fabricated author attrib
330+
await sendUserChanges(socketA, revA, 'Z:6<1*0-1$', fakePool);
331+
const msg = await resultP;
332+
333+
assert.deepEqual(msg, {disconnect: 'badChangeset'},
334+
'Should reject - op with fabricated author to prevent pool injection');
335+
});
303336
});
304337
});

0 commit comments

Comments
 (0)