AID-H-019.002: Secure HTML Rendering & Content Demotion
Pillars: app
Lifecycle: building, operation
Scope boundary
Owns safe rendering and demotion of fetched web content by removing active behavior, extracting bounded text, preserving untrusted provenance, and preventing page-controlled scripts, styles, frames, or resources from crossing into model authority.
AID-H-019.001: AID-H-019.002 sanitizes content after retrieval; AID-H-019.001 authorizes the destination and performs the pinned bounded fetch. Content demotion does not make an unsafe destination fetchable.
AID-H-016.001: AID-H-019.002 strips active web behavior and labels extracted text as untrusted; AID-H-016.001 structures the final prompt so untrusted data cannot syntactically become trusted instruction. Web sanitization and instruction-data separation are sequential defenses.
Implementation guidance
- AID-H-019.002-G001: Sanitize bounded HTML into plain text, preserve its untrusted provenance, and verify that active content cannot cross into model or tool-control context.
AID-H-019.002-G001
Keep active content outside the model boundary
Fetch content server-side with the URL and egress controls from AID-H-019.001. Enforce an organizational byte limit before parsing. Remove executable and embedded elements, strip all remaining markup, and return a typed record whose trust label and provenance are assigned by the gateway rather than supplied by the document.
# File: agent/content_sanitizer.py
from __future__ import annotations
from dataclasses import asdict, dataclass
from hashlib import sha256
import nh3
from bs4 import BeautifulSoup
ACTIVE_TAGS = ("script", "style", "iframe", "object", "embed", "template", "noscript")
@dataclass(frozen=True)
class ExternalText:
schema_version: str
trust: str
source_ref: str
sanitizer_policy_version: str
source_sha256: str
text_sha256: str
text: str
def sanitize_html_content(
raw_html: bytes,
source_ref: str,
*,
max_input_bytes: int,
max_output_bytes: int,
sanitizer_policy_version: str,
) -> dict:
for value, name in (
(max_input_bytes, "max_input_bytes"),
(max_output_bytes, "max_output_bytes"),
):
if isinstance(value, bool) or not isinstance(value, int) or value < 1:
raise ValueError(f"{name} must be a positive policy integer")
if not isinstance(source_ref, str) or not source_ref or len(source_ref) > 512:
raise ValueError("source_ref must be a bounded gateway-issued reference")
if not sanitizer_policy_version:
raise ValueError("sanitizer policy version is required")
if not raw_html or len(raw_html) > max_input_bytes:
raise ValueError("HTML body is empty or exceeds the policy byte limit")
decoded = raw_html.decode("utf-8", errors="strict")
soup = BeautifulSoup(decoded, "html.parser")
for element in soup.find_all(ACTIVE_TAGS):
element.decompose()
tag_free = nh3.clean(str(soup), tags=set(), attributes={})
plain_text = BeautifulSoup(tag_free, "html.parser").get_text("\n", strip=True)
text_bytes = plain_text.encode("utf-8")
if len(text_bytes) > max_output_bytes:
raise ValueError("sanitized text exceeds the policy output limit")
return asdict(ExternalText(
schema_version="aidefend.external-text.v1",
trust="untrusted_external_data",
source_ref=source_ref,
sanitizer_policy_version=sanitizer_policy_version,
source_sha256=sha256(raw_html).hexdigest(),
text_sha256=sha256(text_bytes).hexdigest(),
text=plain_text,
))Preserve role and authority separation
Pass the returned record through a tool or user-data message channel. Never concatenate its text into a system, developer, policy, tool-schema, or authorization message. Treat phrases such as "ignore previous instructions" as untrusted content: sanitization removes active HTML behavior, not the semantic attack. Tool authorization and output validation must remain independent of model compliance.
Verification and evidence
Replay a versioned corpus containing scripts, event handlers, iframes, encoded markup, oversized bodies, malformed UTF-8, delimiter-like text, and benign multilingual pages. Require no active tag or attribute in the emitted text, deterministic byte-limit rejection, a gateway-owned trust label, and proof that the adapter places the record only in the approved message role. Preserve corpus version, sanitizer/library versions, input and output digests, source URL, adapter readback, and explicit PASS/FAIL/ERROR results. Use CDR or isolated conversion for PDF, SVG, and other active document formats rather than treating this HTML routine as a universal parser.
Open-source tools
- nh3
- BeautifulSoup4
- html5lib
- DOMPurify
- Trafilatura (main-text extraction only; active-content sanitization, byte limits, and trust labeling remain external)
Commercial / hosted tools
- Menlo File Security
- OPSWAT MetaDefender
- Glasswall CDR
Threat mappings
MITRE ATLAS
- AML.T0051 LLM Prompt Injection
- AML.T0051.001 LLM Prompt Injection: Indirect
- AML.T0100 AI Agent Clickbait (secure rendering prevents exploitation from fetched malicious content)
- AML.T0078 Drive-by Compromise
MAESTRO
- Compromised RAG Pipelines (L2)
- Input Validation Attacks (L3)
OWASP LLM Top 10 2026
- LLM01:2026 Prompt Injection
OWASP ML Top 10 2023
- N/A
OWASP Top 10 for Agentic Applications 2026
- ASI01:2026 Agent Goal Hijack (content demotion prevents embedded malicious instructions from hijacking goals)
- ASI06:2026 Memory & Context Poisoning (content demotion prevents embedded instructions from poisoning agent context)
NIST Adversarial Machine Learning 2025
- NISTAML.015 Indirect Prompt Injection
- NISTAML.018 Prompt Injection
Cisco Integrated AI Security and Safety Framework
- AITech-1.2 Indirect Prompt Injection (demotes and sanitizes retrieved HTML before model context ingestion)
- AITech-4.2 Context Boundary Attacks (demotes and sanitizes retrieved HTML before model context ingestion)
Google Secure AI Framework 2.0 - Risks
- PIJ: Prompt Injection (content demotion prevents indirect injection via fetched HTML)
Databricks AI Security Framework 3.0
- Model Serving - Inference requests 9.1: Prompt inject (content demotion strips injection payloads from HTML)
- Agents - Core 13.1: Memory Poisoning