Commit 294158e
harden: reject USER_CHANGES inserts without an author attribute (#7773)
* harden: reject USER_CHANGES inserts without an author attribute
Insert ops MUST carry the author attribute reference so that pad.atext.text
and pad.atext.attribs stay in lock-step. An accepted insert with empty
attribs would grow text without contributing matching attribute markers,
leaving the stored AText in a state where the two iterables disagree on
length when reconstructed. Downstream clients then fail reconciliation in
ace2_inner.ts:setDocAText with 'mismatch error setting raw text in
setDocAText' on every subsequent pad load — making the affected pad
effectively unloadable until manually repaired.
This commit adds a single defensive check inside the existing per-op
validation loop in handleUserChanges: when an op is a '+' (insert) and
its attribs string doesn't yield an 'author' entry via
AttributeMap.fromString, reject with badChangeset. The check piggybacks
on the wireApool that was already constructed for the prior author-match
validation, so no extra parsing.
Test fixtures in messages.ts were updated to send proper author-attributed
inserts plus the matching apool (mirroring what the JS web client always
does). A new regression test 'insert without author attribute is rejected'
locks in the new behaviour.
* harden: also close the HTTP API / plugin path via stable system author
The first commit closed the socket.io USER_CHANGES hole. This commit closes
the parallel path through Pad.spliceText (used by API.setText, API.appendText,
the import flow, and plugins like ep_post_data) where an unattributed insert
would otherwise produce a malformed AText.
Approach: instead of REJECTING (which would break ep_post_data and many
existing tests that call setText/appendText without an authorId), substitute
a stable system author when none is provided. The resulting changeset is
properly attributed, the AText stays well-formed, and existing callers
continue to work unchanged. Plugins that want named author attribution
should still pass an explicit authorId (e.g., one allocated via
authorManager.createAuthor).
Pad.SYSTEM_AUTHOR_ID = 'a.etherpad-system' — a stable identifier that
appears in the pad's attribute pool when internal callers (HTTP API,
plugins, server-side imports) write text without naming an author. The
existing 'attribute changes by another author' protections still apply
to socket.io USER_CHANGES paths — a remote client can't impersonate the
system author for inserts (their session author check fires first).
Test:
- Pad.ts spec adds 'spliceText with empty authorId attributes to the
system author' — verifies pad text lands AND the pool contains the
system-author binding. Existing tests that pass an authorId are
unaffected.
* harden: reject USER_CHANGES that would strand the trailing newline
Etherpad's pad text always ends with '\n'. _handleUserChanges previously
appended a separate `nlChangeset` correction revision whenever the
applied USER_CHANGES left the pad without a trailing '\n'. The stored
pad ended up well-formed, but the FIRST NEW_CHANGES broadcast (the
malformed user revision itself) reached browsers BEFORE the correction
did, and applyToAttribution's MergingOpAssembler aborts with
"line assembler not finished" on a non-'\n'-terminated doc — the
watching browser session then dropped the changeset and any subsequent
edits silently no-op'd until the user reloaded.
Replace the silent auto-correction with an explicit reject. Compute
`applyToText(rebasedChangeset, prevText)` before appendRevision; if the
result doesn't end with '\n', throw -> badChangeset disconnect. Clients
must emit USER_CHANGES whose application preserves the invariant —
this matches what the JS web client already does and forces non-JS
clients (etherpad-pad, third-party integrations) to surface their bugs
in their own logs instead of stranding the trailing newline in pad
revision history.
Also fixes a latent retransmission-detection bug surfaced by this PR's
author-attrib changes: moveOpsToNewPool renumbers `*N` references to
whatever slot the pad pool assigns, which can differ from the wire
form's slot. Comparing the raw client wire against the stored revision
form (`changeset === c`) then misses legitimate retransmissions and
the same edit gets duplicated. Snapshot the post-pool-mapping form
(`canonicalCs`) and compare that against `c` instead.
Backend test additions:
- 'changeset that would strand the trailing \\n is rejected' covers
the new rejection path with wire `Z:6>1|1=6*0+1$X` against
`hello\n`.
- handleMessageSecurity test now captures roSocket's own authorId and
uses it in the apool sent through roSocket, because the prior PR
commit made `*0` referencing the wrong author a hard reject.
All 1130 backend tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: bump etherpad-cli-client to ^4.0.3
4.0.3 sends author-attributed inserts and preserves the trailing
newline, complying with this PR's tightened USER_CHANGES validation.
The rate-limit CI workflow drives the test pad via this client, so
without the bump the new server-side rejects fire on the very first
\`pad.append()\` and the rate-limit disconnect never gets a chance to
arrive — testlimits.sh exits 0 instead of 1 and the rate-limit job
fails with "ratelimit was not triggered when sending every 99 ms".
Refs ether/etherpad-cli-client#131
* harden: reject USER_CHANGES that name the reserved system author
The session-author equality check already rejects wire `*N` that
names a different real user, but `a.etherpad-system` is server-
internal — it's only used when spliceText / setText is called with
an empty authorId from HTTP API or plugin paths. A wire op that
names it is either a confused client or an attempt to launder
edits through a reserved attribution slot. Refuse.
Backend test 'insert claiming the reserved system author is
rejected' locks in the new behavior with wire `Z:1>5*0+5$hello`
plus an apool that maps slot 0 to `a.etherpad-system`. All 1131
backend tests pass.
Inline literal `'a.etherpad-system'` rather than importing the
constant from `Pad.SYSTEM_AUTHOR_ID` — `require('../db/Pad')` at
PadMessageHandler module scope returned a partially-initialized
class via the padManager circular path, leaving the static-field
access undefined at runtime.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 2a865a3 commit 294158e
6 files changed
Lines changed: 209 additions & 39 deletions
File tree
- src
- node
- db
- handler
- tests/backend/specs
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
93 | 93 | | |
94 | 94 | | |
95 | 95 | | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
96 | 107 | | |
97 | 108 | | |
98 | 109 | | |
| |||
415 | 426 | | |
416 | 427 | | |
417 | 428 | | |
418 | | - | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
419 | 440 | | |
420 | | - | |
| 441 | + | |
421 | 442 | | |
422 | 443 | | |
423 | 444 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
27 | | - | |
| 27 | + | |
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
| |||
875 | 875 | | |
876 | 876 | | |
877 | 877 | | |
| 878 | + | |
| 879 | + | |
| 880 | + | |
| 881 | + | |
| 882 | + | |
| 883 | + | |
| 884 | + | |
| 885 | + | |
| 886 | + | |
| 887 | + | |
| 888 | + | |
| 889 | + | |
| 890 | + | |
| 891 | + | |
| 892 | + | |
| 893 | + | |
| 894 | + | |
| 895 | + | |
| 896 | + | |
| 897 | + | |
| 898 | + | |
| 899 | + | |
| 900 | + | |
| 901 | + | |
| 902 | + | |
| 903 | + | |
| 904 | + | |
| 905 | + | |
| 906 | + | |
| 907 | + | |
| 908 | + | |
878 | 909 | | |
879 | 910 | | |
880 | 911 | | |
881 | 912 | | |
882 | 913 | | |
883 | 914 | | |
| 915 | + | |
| 916 | + | |
| 917 | + | |
| 918 | + | |
| 919 | + | |
| 920 | + | |
884 | 921 | | |
885 | 922 | | |
886 | 923 | | |
| |||
891 | 928 | | |
892 | 929 | | |
893 | 930 | | |
894 | | - | |
| 931 | + | |
895 | 932 | | |
896 | | - | |
| 933 | + | |
897 | 934 | | |
898 | 935 | | |
899 | 936 | | |
| |||
909 | 946 | | |
910 | 947 | | |
911 | 948 | | |
| 949 | + | |
| 950 | + | |
| 951 | + | |
| 952 | + | |
| 953 | + | |
| 954 | + | |
| 955 | + | |
| 956 | + | |
| 957 | + | |
| 958 | + | |
| 959 | + | |
| 960 | + | |
| 961 | + | |
| 962 | + | |
| 963 | + | |
| 964 | + | |
912 | 965 | | |
913 | 966 | | |
914 | 967 | | |
| |||
920 | 973 | | |
921 | 974 | | |
922 | 975 | | |
923 | | - | |
924 | | - | |
925 | | - | |
926 | | - | |
927 | | - | |
928 | | - | |
929 | 976 | | |
930 | 977 | | |
931 | 978 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
125 | 125 | | |
126 | 126 | | |
127 | 127 | | |
128 | | - | |
| 128 | + | |
129 | 129 | | |
130 | 130 | | |
131 | 131 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
61 | 61 | | |
62 | 62 | | |
63 | 63 | | |
64 | | - | |
| 64 | + | |
65 | 65 | | |
66 | 66 | | |
67 | 67 | | |
68 | 68 | | |
69 | 69 | | |
70 | | - | |
| 70 | + | |
71 | 71 | | |
72 | 72 | | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
73 | 94 | | |
74 | 95 | | |
75 | 96 | | |
| |||
0 commit comments