Dev.to Security 🔐 Cybersecurity 👁 0 📖 7 min read

InboxWall: How a Local-First MCP Server Sanitizes Email HTML to Stop Prompt Injection Before the LLM Sees It

Email is a hostile data source. When you give an AI agent access to your inbox, you hand it HTML that may contain hidden CSS directives, Unicode obfuscation, and invisible text designed to manipulate the LLM's context wi

Email is a hostile data source. When you give an AI agent access to your inbox, you hand it HTML that may contain hidden CSS directives, Unicode obfuscation, and invisible text designed to manipulate the LLM's context window. InboxWall is an open-source MCP server that sanitizes email HTML before the agent sees it, running entirely on your local machine.

The project exposes a key architectural question: where should security boundaries live in agent tool chains? InboxWall's answer is to treat sanitization as a preprocessing step that happens outside the LLM's trust boundary, implemented as a standalone MCP server that other agents can call.

Why Email HTML Is a Prompt Injection Vector

Email clients render HTML, CSS, and Unicode to display formatted messages. Attackers exploit this by embedding:

  • Hidden CSS text: White text on white backgrounds, zero-opacity spans, or off-screen absolute positioning that the LLM reads but humans never see.
  • Unicode tricks: Right-to-left override characters, zero-width joiners, and homoglyphs that change semantic meaning without visual detection.
  • Nested instructions: HTML comments or <style> blocks containing directives like "ignore previous instructions" that only appear in the raw source.

When an agent reads email via IMAP or a mail API, it typically receives the full HTML payload. If that payload goes directly into the LLM's context, the attacker controls part of the prompt.

The MCP Server Architecture

InboxWall runs as a Model Context Protocol server. It exposes a single tool that accepts raw email HTML and returns sanitized plain text. The sanitization pipeline strips:

  • All CSS (inline styles, <style> tags, external stylesheets)
  • JavaScript and event handlers
  • Invisible Unicode characters (zero-width spaces, directional overrides, variation selectors)
  • HTML comments
  • Suspicious attributes (data URIs, srcdoc, formaction)

The server does not connect to email providers. It expects the calling agent to fetch email via IMAP, Exchange Web Services, or a mail API, then pass the HTML body to InboxWall for cleaning.

Local-First Execution Model

InboxWall runs on the same machine as the agent. Email data never leaves the device. This changes the threat model:

  • No cloud preprocessing: You avoid sending email content to a third-party sanitization service.
  • No API keys for sanitization: The MCP server is a local process, not a remote endpoint.
  • Synchronous pipeline: Sanitization happens in-process or over localhost, so latency is measured in milliseconds.

The trade-off is operational overhead. You must run the MCP server as a separate process, manage its lifecycle, and ensure it starts before the agent tries to call it.

Sanitization Pipeline Internals

InboxWall's sanitization logic is a multi-stage filter:

  1. HTML parsing: Parse the email body into a DOM tree using a lenient parser that handles malformed markup.
  2. Tag whitelist: Keep only safe tags (<p>, <a>, <br>, <strong>, <em>, <ul>, <li>, <blockquote>). Drop everything else.
  3. Attribute whitelist: For allowed tags, keep only href on <a> tags. Strip all other attributes.
  4. CSS removal: Delete all style attributes, <style> tags, and <link rel="stylesheet"> elements.
  5. Unicode normalization: Apply NFC normalization, then strip zero-width characters, directional overrides, and variation selectors.
  6. Text extraction: Flatten the sanitized DOM to plain text, preserving line breaks and basic structure.

The output is a plain-text representation of the email with all injection vectors removed.

Example Code Snippet

Here's a simplified version of the Unicode sanitization step:

import unicodedata
import re

ZERO_WIDTH_CHARS = [
    '\u200B',  # Zero-width space
    '\u200C',  # Zero-width non-joiner
    '\u200D',  # Zero-width joiner
    '\u200E',  # Left-to-right mark
    '\u200F',  # Right-to-left mark
    '\u202A',  # Left-to-right embedding
    '\u202B',  # Right-to-left embedding
    '\u202C',  # Pop directional formatting
    '\u202D',  # Left-to-right override
    '\u202E',  # Right-to-left override
    '\uFEFF',  # Zero-width no-break space
]

def sanitize_unicode(text: str) -> str:
    # Normalize to NFC form
    text = unicodedata.normalize('NFC', text)

    # Remove zero-width and directional characters
    for char in ZERO_WIDTH_CHARS:
        text = text.replace(char, '')

    # Remove variation selectors (U+FE00 to U+FE0F)
    text = re.sub(r'[\uFE00-\uFE0F]', '', text)

    return text

This function runs after HTML sanitization but before text extraction. It catches Unicode-based obfuscation that survives HTML parsing.

Security Boundary Placement

InboxWall's design reveals a pattern: security preprocessing should happen before the LLM sees the data, not after. This is the opposite of output filtering, where you sanitize LLM responses to prevent harmful content from reaching users.

Approach Boundary Location Pros Cons
Pre-LLM sanitization Before context window Prevents injection at source; LLM never sees attack vectors Requires separate preprocessing step; adds latency
Post-LLM filtering After generation Simpler pipeline; no preprocessing overhead LLM may already be compromised; harder to detect subtle manipulation
Prompt engineering Inside LLM prompt No external dependencies Unreliable; attackers can craft prompts that override instructions
Retrieval filtering At data fetch time Centralized control; applies to all downstream agents Tight coupling between data source and security logic

InboxWall chooses pre-LLM sanitization because it treats the LLM as untrusted. Even if the model is instructed to ignore hidden text, you cannot rely on prompt engineering to block all attacks. Sanitization must happen outside the model's control.

MCP Server Integration Points

An agent that uses InboxWall follows this flow:

  1. Fetch email: Agent calls IMAP or mail API to retrieve raw email messages.
  2. Extract HTML body: Parse MIME structure to isolate the HTML part.
  3. Call InboxWall MCP tool: Send HTML to the sanitize_email tool exposed by the MCP server.
  4. Receive plain text: InboxWall returns sanitized text with no CSS, JavaScript, or Unicode tricks.
  5. Pass to LLM: Agent includes the sanitized text in the LLM prompt for summarization, classification, or response generation.

The MCP server runs as a separate process. The agent connects to it via stdio or HTTP, depending on the MCP transport configuration. The server does not maintain state between calls. Each sanitization request is independent.

State Management

InboxWall is stateless. It does not cache sanitized emails, track which messages have been processed, or store user preferences. This simplifies deployment but means the agent must handle:

  • Deduplication: If the agent re-fetches the same email, it will re-sanitize it.
  • Error handling: If sanitization fails (malformed HTML, parser crash), the agent must decide whether to skip the email or retry.
  • Rate limiting: The agent controls how many emails it sends to InboxWall per second.

Failure Modes and Observability

InboxWall can fail in several ways:

  • Parser crashes: Extremely malformed HTML may cause the parser to throw an exception. The server should catch this and return an error response, but edge cases exist.
  • Over-sanitization: Aggressive filtering may remove legitimate content. For example, stripping all <a> tags removes links that the agent might need.
  • Under-sanitization: New injection techniques (novel Unicode characters, CSS features) may bypass the filter until the whitelist is updated.
  • Process crashes: If the MCP server dies, the agent loses access to sanitization. The agent must detect this and either restart the server or fail gracefully.

The project does not include built-in observability. You must add logging, metrics, and tracing yourself. Key signals to track:

  • Sanitization latency: Time from receiving HTML to returning plain text.
  • Rejection rate: Percentage of emails that fail sanitization due to parser errors.
  • Bytes removed: Difference between input HTML size and output text size, indicating how much content was stripped.
  • Error types: Categorize failures (parser crash, timeout, invalid input) to identify patterns.

Deployment Shape

InboxWall runs as a standalone process on the same machine as the agent. Typical deployment:

  1. Install dependencies: Python runtime, HTML parser library (e.g., BeautifulSoup or lxml), MCP server framework.
  2. Start MCP server: Launch InboxWall as a background process, listening on stdio or a local port.
  3. Configure agent: Point the agent's MCP client at the InboxWall server (e.g., stdio://inboxwall or http://localhost:8080).
  4. Monitor health: Use a process supervisor (systemd, supervisord) to restart the server if it crashes.

Because the server is local-first, you do not need to configure network firewalls, TLS certificates, or API authentication. The attack surface is limited to the local machine.

When to Use InboxWall

Use it when:

  • Your agent reads email from untrusted senders (customer support, public inboxes, mailing lists).
  • You want a security boundary that does not rely on prompt engineering.
  • You prefer local execution over cloud-based preprocessing.
  • You need a simple, stateless sanitization step that integrates with existing MCP toolchains.

Avoid it when:

  • Your email source is fully trusted (internal corporate email with strict sender policies).
  • You need rich HTML rendering (links, formatting, images) in the agent's context.
  • You already have a cloud-based email security gateway that performs sanitization.
  • You cannot run a separate MCP server process (e.g., serverless agent environments).

Technical Verdict

InboxWall demonstrates that security preprocessing belongs outside the LLM's trust boundary. By running sanitization as a standalone MCP server, it decouples security logic from agent orchestration and makes the filtering rules explicit and auditable.

The local-first architecture keeps email data on-device, which is critical for privacy-sensitive use cases. The trade-off is operational complexity: you must manage the MCP server lifecycle and handle failures when the server is unavailable.

The sanitization pipeline is aggressive. It strips all CSS, JavaScript, and invisible Unicode, which may remove legitimate content. If your agent needs to preserve links or basic formatting, you will need to fork the project and adjust the whitelist.

For agents that process email from untrusted sources, InboxWall is a practical starting point. It shows how to build a security boundary that does not depend on the LLM's ability to resist prompt injection.

Source Links

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.