Files
msd-core/docs/adr/1577-untrusted-input-boundary-and-injection-blocking.md
Jakub Zych a9a7a328e6 refactor: hard-fork GSD -> MSD (Make Software Done)
Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD
across contents and paths, upstream package/repo coordinates -> @golem15/msd-core
and golem15com/msd-core. Deep links into upstream history, sibling upstream
packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is.

Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line,
package/plugin identity, regenerated lockfile, install-tree fixtures, derived
registries and benchmark baseline; migration checksum baseline re-locked
(MSD keeps its own install state, so no install had applied the old sums);
sort-order and regex-escaped expectations in tests adjusted.
2026-10-06 01:47:40 +02:00

5.4 KiB

ADR-1577: Untrusted-input boundary + opt-in injection blocking

  • Status: Accepted — ratified 2026-07-17 (originally Proposed 2026-06-25); see "Ratification" below
  • Issue: #1577
  • Part of: #1573 (harden the agent layer against documented LLM failure modes)

Ratification (2026-07-17): Proposed → Accepted

Ratified by explicit maintainer directive; the Proposed status had gone unconfirmed for 22 days since the ADR landed on 2026-06-25.

Evidence the decision shipped:

  • Issue #1577 is closed (state=CLOSED, stateReason=COMPLETED, closed 2026-06-24T21:07:24Z) as split A of the umbrella #1573, scoped exactly to this ADR's decision.
  • hooks/msd-read-injection-scanner.js:118 extends the scanner to SCANNED_TOOLS = new Set(['Read', 'WebFetch', 'WebSearch']), wired via hooks/hooks.json:34's "Read|WebFetch|WebSearch" matcher — closing the WebFetch/WebSearch gap named in Context.
  • hooks/msd-read-injection-scanner.js:212 gates blocking on cfg.security?.injection_blocking === true, read directly via fs.readFileSync/JSON.parse (independent of src/configuration.cts's key whitelist, so no drop risk).
  • security.injection_blocking is a registered config key end-to-end: msd-core/bin/shared/config-schema.manifest.json:109 lists it and msd-core/bin/shared/config-defaults.manifest.json:103 defaults it false; src/configuration.cts:47 builds VALID_CONFIG_KEYS from that manifest and src/config-schema.cts:61 (isValidConfigKey) consults it.
  • msd-core/references/untrusted-input-boundary.md exists and is @-included by exactly the 10 ingest agents named in the Decision: msd-advisor-researcher, msd-ai-researcher, msd-assumptions-analyzer, msd-doc-classifier, msd-doc-synthesizer, msd-domain-researcher, msd-phase-researcher, msd-project-researcher, msd-research-synthesizer, msd-ui-researcher.
  • docs/explanation/security-model.md:150-165 documents the PostToolUse pre-filter framing and names all 10 agents; docs/CONFIGURATION.md:910 documents security.injection_blocking with a direct link to ADR-1577.
  • tests/read-injection-scanner.security.test.cjs runs SCAN-WF-01, SCAN-WF-02, SCAN-WF-03, and SCAN-WS-01 against the real hook subprocess for WebFetch/WebSearch payloads and asserts real detections.
  • tests/injection-blocking-config.test.cjs asserts isValidConfigKey('security.injection_blocking') is true, isValidConfigKey('security') is false, and CONFIG_DEFAULTS.security.injection_blocking === false.

Governance state: owning issue #1577 — CLOSED, stateReason COMPLETED, closed 2026-06-24T21:07:24Z.

Context

The research/doc-ingest agents concatenate text returned by WebFetch / WebSearch / Read into their context with no data/instruction separation, and the msd-read-injection-scanner hook only scanned the Read tool — leaving WebFetch/WebSearch (the largest untrusted channel) unscanned. Prompt injection via fetched content is a documented LLM failure mode (arXiv 2506.05739, 2507.15219, 2504.20472).

Two mechanisms were considered for the hook-level control:

  1. Redaction — strip the detected content before it reaches the model. This requires hookSpecificOutput.updatedToolOutput, which is unused anywhere in this repo and not verifiable in CI for a PostToolUse hook. Claiming redaction the code can't reliably perform would re-introduce exactly the overclaim this work set out to remove.
  2. Circuit-breaker — a PostToolUse hook that, after the fetch has executed and the content is already in the transcript, emits decision: "block" to halt the agent's next step. It does not redact content already in context.

Decision

  • Extend the scanner to match Read | WebFetch | WebSearch, documented honestly as a pattern-based pre-filter, not a model-level guard.
  • Make the prompt-level boundary the primary control: a shared msd-core/references/untrusted-input-boundary.md, @-included by the 10 ingest agents, instructs treat-fetched-text-as-data, self-scan before use, task-anchoring, and a fresh random delimiter per quoted wrap. This is the layer that keeps an injection from being followed even while it sits in context.
  • Ship hook-level blocking as an opt-in circuit-breaker: security.injection_blocking (a registered config key; default advisory). Documentation states plainly that enabling it halts further processing on a HIGH detection — it does not retroactively redact the already-fetched content. Redaction via updatedToolOutput is deferred until that field's behavior is verifiable in this runtime.

Consequences

  • Non-breaking. The default posture is advisory; no existing default changes. Blocking is reached only by an explicit opt-in.
  • The strongest guarantee is prompt-level (data/instruction separation), which is unenforced at runtime — this is defense-in-depth (arXiv 2503.00061), not a hard sandbox. A determined adaptive attacker or a weaker model may still be influenced.
  • Localized docs are managed separately; only the canonical English docs/explanation/security-model.md is updated here.
  • Follow-up: if/when updatedToolOutput redaction is confirmed supported, the circuit-breaker can be upgraded to an actual redactor without changing the opt-in surface.