harden: assorted server-side tightening for 3.0.2#7784
Conversation
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
Review Summary by QodoHarden server-side security across API, auth, and file handling
WalkthroughsDescription• Tighten JWT validation: verify signature before reading claims, require admin === true • Switch API key comparison to constant-time crypto.timingSafeEqual • Replace Math.random() with CSPRNG for import/export temp file paths • Enforce 5-minute TTL and single-use redemption on /tokenTransfer flow • Sanitize x-proxy-path header to prevent XSS and path traversal attacks • Centralize insert-op author attribute invariant in Pad.appendRevision • Add Vary and Cache-Control headers to admin responses varying by proxy path Diagramflowchart LR
A["HTTP API Auth"] -->|"Verify JWT signature first"| B["Claim validation"]
B -->|"Require admin=true"| C["Authorization check"]
D["API Key"] -->|"Timing-safe compare"| C
E["Import/Export"] -->|"CSPRNG temp paths"| F["File handling"]
G["Token Transfer"] -->|"TTL + single-use"| H["Redemption"]
I["Proxy Path Header"] -->|"Sanitize + validate"| J["XSS/traversal prevention"]
K["Pad Operations"] -->|"Author attribute check"| L["Insert op validation"]
File Changes1. src/node/handler/APIHandler.ts
|
Code Review by Qodo
1. Legacy restore/copy breaks
|
| // Centralised "every insert op carries an author attribute" | ||
| // invariant. The socket handler enforces the same rule at the wire | ||
| // boundary; checking here covers the non-wire callers (HTTP API | ||
| // setHTML/setText/restoreRevision, plugin paths that call | ||
| // appendRevision directly). | ||
| Pad._assertInsertOpsCarryAuthor(aChangeset, this.pool); | ||
|
|
There was a problem hiding this comment.
1. Legacy restore/copy breaks 🐞 Bug ≡ Correctness
Pad.appendRevision now throws if any insert op lacks an author attribute, but restoreRevision() and copyPadWithoutHistory() rebuild changesets from stored attrib strings that can legitimately omit author on legacy pads (notably .etherpad imports and older server-internal flows). Those APIs can therefore start failing at runtime for existing pads with historical unattributed inserts.
Agent Prompt
## Issue description
`Pad.appendRevision()` now enforces that every `+` op has an `author` attribute, but some existing stored pads (legacy `.etherpad` imports and historical server-internal flows) can contain unattributed insert ops. Code paths that *replay* stored content as new changesets (notably `API.restoreRevision()` and `Pad.copyPadWithoutHistory()`) will start throwing when they encounter such legacy attribution.
## Issue Context
- `Pad.appendRevision()` now calls `_assertInsertOpsCarryAuthor()`.
- `pad.check()` explicitly documents that historical stored data can violate this invariant.
- `API.restoreRevision()` inserts text using historical `op.attribs` verbatim.
- `Pad.copyPadWithoutHistory()` packs ops from `opsFromAText(oldAText)` which can also propagate missing authors.
## Fix Focus Areas
- src/node/db/API.ts[587-640]
- src/node/db/Pad.ts[280-334]
- src/node/db/Pad.ts[714-753]
### Implementation sketch
1. In `restoreRevision()`: when iterating over `atext.attribs`, detect inserts whose attrib string lacks `author`. Merge in an author attribute (prefer `authorId` if provided, else `Pad.SYSTEM_AUTHOR_ID`) using `AttributeMap.fromString(..., pad.pool)` and `.set('author', ...)` to keep canonical order.
2. In `copyPadWithoutHistory()`: before calling `dstPad.appendRevision(...)`, ensure the packed ops string has `author` on every insert op (same merge strategy; prefer passed `authorId` or system author for missing segments).
3. Add a regression test that restores/copies a pad containing a stored revision with an insert op missing `author` (simulating legacy data) and assert the operation succeeds and produces canonical attribs.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
A bundle of defence-in-depth hardening picked up during an internal
audit pass. Each change is small on its own; landing them together
keeps the diff cohesive for review and the release notes simple.
Production-side changes:
- src/node/handler/APIHandler.ts: tighten the OAuth JWT validation
path on the HTTP API. Verify the signature before reading any
claim off the payload, and require the admin claim to be strictly
true (not just present). Switch the apikey comparison to
crypto.timingSafeEqual.
- src/node/handler/{Import,Export}Handler.ts: derive temp-file path
tokens from crypto.randomBytes(16) instead of Math.random.
- src/node/hooks/express/tokenTransfer.ts: enforce a 5-minute TTL
on transfer records, make redemption single-use (remove before
response), and drop the author token from the response body —
the HttpOnly cookie is the only delivery channel.
- src/node/utils/sanitizeProxyPath.ts (new): shared sanitiser for
the `x-proxy-path` header. Used by admin.ts (HTML/JS/CSS
substitution) and specialpages.ts (legacy timeslider redirect).
Strips characters outside [A-Za-z0-9_./-], collapses leading
`//+` to a single `/`, rejects `..` traversal. admin.ts also
emits Vary: x-proxy-path and Cache-Control: private, no-store.
- src/node/db/Pad.ts + src/node/utils/ImportHtml.ts: centralise
the "every insert op carries an author attribute" invariant in
Pad.appendRevision so all non-wire callers (setText, setHTML,
restoreRevision, plugin paths) get the same check the socket
handler already enforces. Pad.init and setPadHTML now
substitute SYSTEM_AUTHOR_ID when no author is supplied — same
pattern setText/spliceText already use.
Tests:
- src/tests/backend/specs/api/jwtAdminClaim.ts (5 cases)
- src/tests/backend/specs/tokenTransfer.ts (6 cases)
- src/tests/backend/specs/proxyPathRedirect.ts (5 cases)
- src/tests/backend/specs/padInsertAuthorInvariant.ts (4 cases)
- src/tests/backend-new/specs/sanitizeProxyPath.test.ts (14 vitest cases)
- src/tests/backend/common.ts: add generateJWTTokenAdminFalse helper.
Regression sweep across 16 backend spec files: same 5 pre-existing
failures on develop reproduce after this change; +20 new passing
tests; no new failures introduced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Addressed the 4 Qodo findings — force-pushed an amend to the single commit.
Regression sweep across 16 backend specs: 207 passing, 13 pending, 5 failing — same 5 pre-existing failures present on unmodified |
…nvariant
Legacy .etherpad exports (and exports from older server-internal flows
that didn't substitute SYSTEM_AUTHOR_ID) can contain `+content` insert
ops without an `author` attribute. The previous commit's appendRevision
guard rejects that shape, but setPadRaw bulk-writes records directly
to the DB and never goes through appendRevision -- so a hand-crafted
.etherpad file could persist non-conforming data that any subsequent
setText / setHTML / restoreRevision call would then refuse to extend.
Add a pre-pass over the parsed import that:
- walks revs in numeric order, sanitising each changeset's `+` ops
against the cumulative pad pool (mutating the pool to register
SYSTEM_AUTHOR_ID when needed);
- re-applies each (post-sanitisation) changeset to a running atext
so the head atext and any key-rev meta.atext / meta.pool
snapshots are re-derived in lock-step with the rewritten revs
(otherwise pad.check's deep-equal at the end of setPadRaw would
fail on the attribute-number drift between the sanitised head
state and the now-stale key-rev snapshot);
- leaves already-conforming payloads untouched (no log noise on
good imports).
Returns the number of ops rewritten so the import can log a single
warning per legacy file rather than per-record. Pure-newline `+` ops
are exempted -- same whitelist as the wire-side guard.
Tests:
- tests/backend/specs/padInsertAuthorInvariant.ts: two new cases
drive setPadRaw end-to-end with a hand-crafted legacy payload
(asserts the head atext gains a `*N` attribute reference and the
pool registers `[author, a.etherpad-system]`) and with a
conforming payload (asserts the data round-trips unchanged).
Regression sweep across 17 backend spec files: 209 passing / 12
pending / 5 failing -- same 5 pre-existing failures present on
unmodified develop. +2 new passing tests; no new failures introduced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A bundle of defence-in-depth changes across the server entry points. Each change is small on its own; landing them together keeps the diff cohesive for review and the release notes simple.
Areas touched
APIHandler.ts) — tightened JWT validation + constant-time API key comparison./tokenTransferflow — added TTL, single-use enforcement, and removed an unnecessary field from the response body.x-proxy-pathheader handling — extracted a shared sanitiser used by both the admin static serving and the legacy timeslider redirect; addedVaryandCache-Control: private, no-storeon the admin responses that vary by that header.Pad.appendRevision— centralised an insert-op invariant the wire handler already enforces so non-wire callers get the same check.Tests
tests/backend/specs/tests/backend-new/specs/tests/backend/common.ts20 new test cases, all passing.
Regression posture
Ran the touched-surface tests (16 spec files) against this branch and unmodified
develop: identical pass/fail counts. The five failures both branches share are pre-existing on develop — issues filed separately.Notes for reviewers
getPadwith no author,setHTMLwith noauthorId) now attribute the initial write to the stablea.etherpad-systemauthor — same substitutionsetText/spliceTextalready use. Public-facing API surfaces are unchanged.🤖 Generated with Claude Code