AID-H-019.002: Secure HTML Rendering & Content Demotion

Version 1.20261015 · Actionable sub-technique

Strip scripts, styles, iframes, and active content; extract bounded plain text; and carry it to the model only as explicitly untrusted data. This prevents active browser content from crossing the rendering boundary and reduces the authority of indirect prompt-injection text, but does not make semantic instructions trustworthy or replace downstream tool authorization.

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.
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

Commercial / hosted tools

Threat mappings

MITRE ATLAS

MAESTRO

OWASP LLM Top 10 2026

OWASP ML Top 10 2023

OWASP Top 10 for Agentic Applications 2026

NIST Adversarial Machine Learning 2025

Cisco Integrated AI Security and Safety Framework

Google Secure AI Framework 2.0 - Risks

Databricks AI Security Framework 3.0

AID-H-019.002: Secure HTML Rendering & Content Demotion | AIDEFEND

Loading interactive view…

A Practical Knowledge Base for AI Security Defenses

Version