resolve() accepts a single exact occurrence without a context check — a duplicated block can hijack any anchor consumer #74

Open
opened 2026-07-03 01:20:31 +00:00 by benstull · 0 comments
Owner

Surfaced by the #70 branch review (session 0066, PR #73) and CONFIRMED against the code; pre-existing (pre-dates #70), affects accept and reject alike.

Behavior (src/anchorer.ts resolve()): when the fingerprint text occurs exactly once in the document, resolve returns that occurrence without checking the before/after context — context disambiguation only runs for multiple occurrences (if (occ.length === 1) return rangeAt(occ[0], fp.text);).

Failure scenario: the writer copy-pastes a block containing an applied proposal (or any anchored text), then edits the ORIGINAL site so it no longer matches. The fingerprint now has exactly one exact occurrence — the untouched duplicate — so every anchor consumer (accept, reject, thread/attribution re-anchoring) resolves to the WRONG site: e.g. ✗ Reject replaces the duplicate paragraph with the proposal original while the real applied-and-tweaked text stays, and the proposal record is removed, so the corruption is unflagged.

Mitigations in place: #70 made the tracked-span bookkeeping (appliedSpans) ground truth while present — renderAll only rebuilds an absent entry from a resolve — so the reject fallback path is insulated in-session. The primary resolved paths are not.

Fix direction to evaluate: require contextMatches even for a single occurrence (fall back to lineHint/orphaned when the context disagrees), weighed against re-anchoring tolerance for legitimately moved text (INV-1/INV-6 external-move re-anchor cases in test/anchorer.test.ts and proposals.test.ts).

Surfaced by the #70 branch review (session 0066, PR #73) and CONFIRMED against the code; **pre-existing** (pre-dates #70), affects accept and reject alike. **Behavior (`src/anchorer.ts` `resolve()`):** when the fingerprint text occurs exactly once in the document, `resolve` returns that occurrence **without** checking the `before`/`after` context — context disambiguation only runs for multiple occurrences (`if (occ.length === 1) return rangeAt(occ[0], fp.text);`). **Failure scenario:** the writer copy-pastes a block containing an applied proposal (or any anchored text), then edits the ORIGINAL site so it no longer matches. The fingerprint now has exactly one exact occurrence — the untouched duplicate — so every anchor consumer (accept, reject, thread/attribution re-anchoring) resolves to the WRONG site: e.g. ✗ Reject replaces the duplicate paragraph with the proposal original while the real applied-and-tweaked text stays, and the proposal record is removed, so the corruption is unflagged. **Mitigations in place:** #70 made the tracked-span bookkeeping (`appliedSpans`) ground truth while present — `renderAll` only rebuilds an absent entry from a resolve — so the reject fallback path is insulated in-session. The primary resolved paths are not. **Fix direction to evaluate:** require `contextMatches` even for a single occurrence (fall back to lineHint/orphaned when the context disagrees), weighed against re-anchoring tolerance for legitimately moved text (INV-1/INV-6 external-move re-anchor cases in `test/anchorer.test.ts` and `proposals.test.ts`).
benstull added the priority/P3type/bug labels 2026-07-03 01:20:31 +00:00
Sign in to join this conversation.