DOM2 · Security layer

The ingestion boundary is empty.
We fill it.

Every model is 100% exposed to indirect prompt injection because nobody filters the web content an agent reads. DOM2 sits at that boundary: it renders the page, sees what a human actually sees, and hands the agent a structured JSON — clean visible content delivered, hidden injections quarantined.

LIVE · POST dom2.srv1000934.hstgr.cloud/clean

One attack page → three buckets

delivered

8
Visible clean body + relevant metadata (title, alt, keywords). All the agent reads.
Amazing Product
A normal product page…
metadata keywords: great product…

quarantined

13
Hidden concealment / directive content. Kept for audit, never fed to the agent.
alt: "Ignore previous instructions…"
color-match: "Forget your rules…"
meta desc: "Disregard your instructions…"

stripped

1
Benign noise only — mobile duplicates, boilerplate, machine attrs. Counted, not carried.
boilerplate <noscript> / comments
machine_attr data-*, hidden inputs
duplicate mobile-view copy

13 / 13 injection vectors caught · 0 reached the agent · relevant metadata kept

Calibration on real pages

PageNodesDeliveredQuarantinedInjections caughtFalse positives
Attack page (14 vectors)2681313 / 13
apple.com189145500
stripe.com303470900

The layer stack

0

Fetch guard guard.py

SSRF hardening: blocks private / link-local / reserved IPs (incl. 169.254.169.254), scheme allowlist, per-request DNS re-validation, redirect & node caps.

1

Normalization normalize.py

Strips tag chars, zero-width, bidi, variation selectors; caps combining marks; NFKC; confusables. Emits normalization_delta; forwards the normalized text, never the original.

2

Technique detector.py

How it's hidden: display / opacity / size / offscreen / clip, WCAG color camouflage, sr-only, and ingestible channels — alt, aria, meta, title, hidden inputs, data-*.

2b

Intent intent.py

Why hidden: responsive / accessibility / interactive / structural / icon (benign) vs concealment. Orthogonal directive axis, multilingual.

3

Instruction bank prompt_guard.py

Multilingual (EN / DE / FR / ES / RU) regex — used as a scored feature, never a standalone verdict. Benchmarks show it's unusable alone (57–100% false positives).

4

Partitioner sanitize.py

One pass → clean_text + delivered / stripped / quarantined. Keeps relevant metadata (title, alt, description, keywords); strips only noise (duplicates, boilerplate, machine attrs).

What it promises

This layer reduces incidence and provides provenance + quarantine. It does not claim to "block prompt injection" — no single layer can. False confidence is the worst outcome in security, so the product promises exactly what it delivers: a cleaner, attributed input.

Honest limitations

  • Directive detection is regex-bounded. EN/DE/FR/ES/RU covered; paraphrases outside the bank slip the concealment verdict — full coverage needs a Layer-3 classifier (planned, async on quarantined blocks only).
  • DNS-rebinding TOCTOU is mitigated by per-request re-validation, not fully closed — full closure needs Chromium IP pinning.
  • Throughput is bound by the headless render (~5 s/page on 1 vCPU), not the detection (<1%). Levers: URL cache (live, 300 s), context pool + more cores.
  • Visible boilerplate (nav / footer / ads) is still delivered — readability stripping is a separate, opt-in layer.
Reduces incidence · provides provenance · does not claim to block injection. DOM2 Security · /clean /sanitize /detect