[Security 6/9] Server-side citation verification against source text - #249
Open
amal66 wants to merge 1 commit into
Open
[Security 6/9] Server-side citation verification against source text#249amal66 wants to merge 1 commit into
amal66 wants to merge 1 commit into
Conversation
amal66
force-pushed
the
olp-pr/sec-citation-verify
branch
from
July 25, 2026 21:31
f73fdfa to
6bb5cc4
Compare
Port of the fork's document-quote verification (apps/api/src/lib/tools/ verifyCitations.ts and its wiring) to upstream layout. After the model's <CITATIONS> block is parsed, each document quote is located in the document's extracted source text (exact, then whitespace/case-tolerant, then punctuation-tolerant matching). Quotes get a per-quote verification record and each citation an aggregate verification_status: verified | repaired (exact source excerpt swapped in) | unverified. Case-law citations pass through untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CEguyEgXa9JjCciXCcVemC
amal66
force-pushed
the
olp-pr/sec-citation-verify
branch
from
August 3, 2026 01:57
6bb5cc4 to
a05e295
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
[Security 6/9] Server-side citation verification against source text
TL;DR
Every quoted citation the model produces is checked server-side against the actual document text — pure deterministic string matching, no extra LLM calls. Quotes that match exactly are
verified; quotes that match under whitespace/case/punctuation tolerance arerepaired(and the exact source text is swapped in); quotes that don't appear areunverified. A missing status is treated as untrusted by the UI.Risk to user data
Severity: high — integrity, not confidentiality. The adversary here is the model itself. LLMs fabricate plausible-looking quotes and attributions. When the app presents a quote as coming from a document, that is an integrity claim, and in legal work a fabricated citation is a sanctionable harm. This is also the third defense-in-depth layer against prompt injection ([5/9]): even if an injection makes the model assert something, a quote it attributes to a document must actually be in that document to be shown as verified.
Flows affected
streaming.ts).verifyCitations.tsruns after the model's<CITATIONS>block is parsed.emitEvents:false, memoized perdoc_id, at most one read per document per turn. Case-law citations (verified upstream via CourtListener) pass through untouched.Attack precedent
Possible fixes, and what we chose
flowchart TD Q["model's quoted citation"] --> M{exact substring<br/>in source text?} M -- yes --> V["verified"] M -- no --> T{match under whitespace/<br/>case/punctuation tolerance?} T -- yes --> R["repaired<br/>(swap in exact source excerpt)"] T -- no --> U["unverified<br/>(preserve model text, flag untrusted)"]Design subtleties: a repaired quote has the exact source excerpt substituted in, so drifted text is never surfaced as the source's words. Cross-page quotes (joined by a
[[PAGE_BREAK]]sentinel) are split and each segment verified independently. Unreadable-source sentinels are treated as "no source" so a quote falls back tounverifiedrather than false-matching the error string. Custom builders (tabular) that bypass verification carry no status, and the UI treats a missing status as untrusted.What's in this PR
backend/src/lib/chat/verifyCitations.ts— the tiered matcher (new).backend/src/lib/chat/types.ts—CitationVerificationStatus/QuoteVerification.backend/src/lib/chat/streaming.ts,tools/documentOps.ts— wire verification into finalization.verifyCitations.test.ts(16 tests: exact/repaired/unverified, cross-page, unreadable source).Reading
Mata v. Avianca · OWASP Top 10 for LLM Apps — overreliance / hallucination
Known limitation (from a post-open adversarial re-review)
Tier-3 tolerant matching strips punctuation without inserting a separator, so digit tokens can collapse (
1.2→12): a quote citing "Section 1.2" can tier-3 match a source that only contains "Section 12". Tiers 1–2 (exact and whitespace/case-normalized) are unaffected. The tight fix — inserting a separator when punctuation sits between digits — touches the sharednormalizeWithMapused byfind_in_document, so I'd rather land it as a small follow-up with its own tests than widen this diff. Flagging it now so the tier-3 tolerance is reviewed with eyes open.