Skip to content

security: validate deserialized annotation data on load#58

Merged
sam-powers merged 1 commit into
mainfrom
security/validate-deserialized-annotations
Jun 13, 2026
Merged

security: validate deserialized annotation data on load#58
sam-powers merged 1 commit into
mainfrom
security/validate-deserialized-annotations

Conversation

@sam-powers

Copy link
Copy Markdown
Owner

What

Closes the gap where Quill trusted deserialized data far more than rendered data. The sidecar (.comments.json) and crash-recovery draft (draft.json) are JSON read back from disk — not always Quill's own well-formed output. They can be hand-edited, truncated by the crash they exist to recover from, or (the sidecar sits beside a potentially shared .md) supplied by someone else.

The editor trusts annotation positions structurally: a comment's from/to flow into doc.resolve, which throws on a negative, fractional, or NaN position — white-screening the app on open. normalizeSidecar previously validated nothing (comments: parsed.comments ?? []) and the draft loader checked only the envelope, not the payload arrays.

Changes

  • src/utils/annotationValidation.ts — one source of truth for "what a valid annotation looks like." sanitizeComments / sanitizeSuggestions drop non-array input and any record that fails the shape check, coercing every from/to to a finite non-negative integer (ordered from <= to) so it can never throw in the editor. sanitizeAISession is all-or-nothing — a partial binding can't resume a conversation and would only mislead the UI.
  • normalizeSidecar and the draft loader both route through it, so the two deserialization boundaries enforce identical guarantees. A malformed record is dropped, not fatal — genuine recoverable data still loads.

Tests

30-case unit suite for the validators (bad/negative/NaN/fractional positions, wrong types, mixed good-and-bad arrays, partial bindings, default coercion). Full bar green: typecheck, eslint, prettier, vitest (250, +19).

🤖 Generated with Claude Code

The sidecar (.comments.json) and crash-recovery draft (draft.json) are
JSON files read back from disk that aren't always Quill's own
well-formed output: they can be hand-edited, truncated by a crash, or —
since the sidecar sits beside a potentially shared .md — supplied by
another party. The editor trusts annotation positions structurally: a
comment's from/to flow into doc.resolve, which throws on a negative,
fractional, or NaN position, white-screening the app on open.

normalizeSidecar previously validated nothing (comments: parsed.comments
?? []) and isValidDraft checked only the envelope, not the payload
arrays. Either path could load a record that crashes the editor.

- New utils/annotationValidation.ts is the single source of truth for
  "what a valid annotation looks like": sanitizeComments /
  sanitizeSuggestions drop non-array input and any record that fails the
  shape check, coercing every from/to to a finite non-negative integer
  (ordered from <= to) so it can't throw in the editor. sanitizeAISession
  is all-or-nothing (a partial binding can't resume a conversation).
- normalizeSidecar and the draft loader both route through it, so the
  two deserialization boundaries enforce identical guarantees. A
  malformed record is dropped, not fatal — recoverable data still loads.

Adds a 30-case unit suite for the validators (bad positions, wrong
types, mixed good/bad arrays, partial bindings).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sam-powers sam-powers merged commit 643e561 into main Jun 13, 2026
5 of 6 checks passed
@sam-powers sam-powers deleted the security/validate-deserialized-annotations branch June 13, 2026 21:57
sam-powers added a commit that referenced this pull request Jun 13, 2026
Documents Quill's security posture for a reviewing engineer: the threat
model (deep link as the one attacker-influenced entry point; deserialized
data trusted more than rendered data), the four merged hardening measures
(#57 path confinement + deep-link validation, #58 annotation sanitization,
#59 URL scheme allowlists, #60 binary-resolution hardening), and the two
deliberate non-fixes left in place with rationale (asset-protocol $HOME/**
scope, remote-image beacon).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant