ai security

Detecting Indirect Prompt Injection

Detect and defend against prompt injection hidden in documents, web pages, and images consumed by an agent.

agent-securityai-securitycontent-scanningindirect-prompt-injectionllm-defensellm-guardmultimodalowasp-llm
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

Authorized-use-only notice: Scripts in this skill scan untrusted content for injection payloads and run detector models. Run scanning only on data you are authorized to process, and treat any extracted payloads as live untrusted input — never paste them back into a privileged LLM context.

Overview

Indirect prompt injection (MITRE ATLAS AML.T0051.001, OWASP LLM01:2025) occurs when an LLM-powered agent ingests external content — a web page it browses, a PDF or email it summarizes, an image it OCRs, a tool result it reads — and that content contains hidden instructions the model then follows as if they came from the developer or user. Because the agent treats all tokens in its context window as equally authoritative, an attacker who controls any consumed artifact can hijack the agent's behavior: exfiltrate conversation history, redirect tool calls, leak secrets, or pivot through connected systems.

Unlike direct injection (the user types the attack), indirect injection arrives through a trusted-looking data channel, which is why naive input filtering misses it. Payloads hide in many forms: HTML comments and display:none/zero-width text on web pages, white-on-white or tiny-font text in PDFs, alt-text and EXIF metadata in images, text rendered into pixels (invisible to OCR-light filters but read by multimodal models), Unicode tag/zero-width characters, and Base64/ROT13 obfuscation. This skill builds a detection pipeline that normalizes and scans every artifact before it reaches the model, combining heuristic/regex detection, dedicated detector models (Meta Prompt Guard 2, ProtectAI's deberta-v3 prompt-injection classifier via LLM Guard), and multimodal extraction for images, and then defines response actions and detection telemetry.

When to Use

  • When building or hardening an agent that browses the web, reads email, summarizes documents, or processes user-uploaded files/images.
  • When you need a content-sanitization gate in front of an LLM that ingests third-party data.
  • During AI red-team / blue-team exercises validating that injected instructions in retrieved artifacts are caught.
  • When investigating an incident where an agent behaved as if it received instructions you did not author.
  • As a CI/CD pre-ingestion scan for documents added to a knowledge base.

Prerequisites

  • Python 3.10+ and a virtual environment.
  • Install the detection tooling:
python -m venv .venv && source .venv/bin/activate
 
# LLM Guard — input/output scanners incl. PromptInjection
pip install llm-guard
 
# Hugging Face transformers for Prompt Guard 2 / deberta classifiers
pip install transformers torch
 
# Content extraction: HTML, PDF, images
pip install beautifulsoup4 pypdf pillow pytesseract
# pytesseract requires the Tesseract OCR engine:
#   Debian/Ubuntu: sudo apt-get install -y tesseract-ocr
#   macOS:         brew install tesseract
#   Windows:       choco install tesseract
  • Access (gated) to meta-llama/Llama-Prompt-Guard-2-86M on Hugging Face, or use the open protectai/deberta-v3-base-prompt-injection-v2 classifier.

Objectives

  • Extract human-invisible and obfuscated text from web pages, PDFs, and images.
  • Normalize content (strip zero-width chars, decode Base64/ROT13, flatten Unicode) before scanning.
  • Run heuristic and ML-based injection detectors (LLM Guard PromptInjection scanner, Prompt Guard 2).
  • Score each artifact and enforce a block / sanitize / allow decision before model ingestion.
  • Emit structured detection telemetry suitable for a SIEM and map findings to ATLAS AML.T0051.001.

MITRE ATT&CK Mapping

ID Official Name Relevance
AML.T0051.001 LLM Prompt Injection: Indirect The exact technique this skill detects and mitigates
AML.T0051 LLM Prompt Injection Parent technique covering all prompt-injection variants
AML.T0057 LLM Data Leakage Common objective of an indirect injection that this detection prevents
AML.T0053 LLM Plugin Compromise Injected instructions frequently target the agent's tools/plugins

Workflow

1. Extract hidden text from web content

Pull comments, hidden elements, and metadata that a human never sees but the model does.

# extract_html.py
from bs4 import BeautifulSoup, Comment
 
def extract_hidden(html: str):
    soup = BeautifulSoup(html, "html.parser")
    hidden = []
    for c in soup.find_all(string=lambda t: isinstance(t, Comment)):
        hidden.append(("comment", c.strip()))
    for el in soup.select('[style*="display:none"],[style*="visibility:hidden"],[hidden]'):
        hidden.append(("css-hidden", el.get_text(strip=True)))
    for img in soup.find_all("img"):
        if img.get("alt"):
            hidden.append(("alt-text", img["alt"]))
    return [h for h in hidden if h[1]]

2. Normalize and de-obfuscate

Strip zero-width / Unicode-tag characters and decode common encodings so detectors see the real payload.

# normalize.py
import base64, codecs, re, unicodedata
 
ZERO_WIDTH = dict.fromkeys(map(ord, "​‌‍⁠"), None)
TAG_RANGE = range(0xE0000, 0xE0080)  # Unicode tag chars used to smuggle text
 
def normalize(text: str) -> str:
    text = text.translate(ZERO_WIDTH)
    text = "".join(ch for ch in text if ord(ch) not in TAG_RANGE)
    text = unicodedata.normalize("NFKC", text)
    for token in re.findall(r"[A-Za-z0-9+/=]{20,}", text):
        try:
            decoded = base64.b64decode(token).decode("utf-8", "ignore")
            if decoded.isprintable():
                text += f"\n[decoded-b64] {decoded}"
        except Exception:
            pass
    text += "\n[decoded-rot13] " + codecs.decode(text, "rot_13")
    return text

3. Scan with LLM Guard's PromptInjection scanner

LLM Guard wraps a transformer classifier and returns a risk score per input.

# scan_llmguard.py
from llm_guard.input_scanners import PromptInjection
from llm_guard.input_scanners.prompt_injection import MatchType
 
scanner = PromptInjection(threshold=0.5, match_type=MatchType.FULL)
 
def scan(text: str):
    sanitized, is_valid, risk = scanner.scan(text)
    return {"is_valid": is_valid, "risk": risk}  # is_valid=False => injection detected

4. Add a dedicated detector model (Prompt Guard 2 / deberta)

Run Meta Prompt Guard 2 (or the open ProtectAI deberta classifier) for a second opinion.

# detector_model.py
from transformers import pipeline
 
# Open classifier (no gating); swap to meta-llama/Llama-Prompt-Guard-2-86M if licensed
clf = pipeline("text-classification",
               model="protectai/deberta-v3-base-prompt-injection-v2")
 
def is_injection(text: str, threshold: float = 0.5) -> bool:
    out = clf(text[:512])[0]
    return out["label"].upper() == "INJECTION" and out["score"] >= threshold

5. Extract and scan text rendered inside images

Multimodal agents read text painted into pixels; OCR it and run the same scanners.

# scan_image.py
from PIL import Image
import pytesseract
 
def ocr(path: str) -> str:
    return pytesseract.image_to_string(Image.open(path))
# Feed ocr(path) through normalize() + scan() + is_injection()

6. Enforce a decision and emit telemetry

Combine signals into block / sanitize / allow, and log a structured event for the SIEM.

# decide.py
import json, hashlib
from datetime import datetime, timezone
 
def decide(source, raw, normalized, llmguard_invalid, model_flag):
    flagged = llmguard_invalid or model_flag
    event = {
        "ts": datetime.now(timezone.utc).isoformat(),
        "source": source,
        "sha256": hashlib.sha256(raw.encode("utf-8", "ignore")).hexdigest(),
        "atlas": "AML.T0051.001",
        "llmguard_injection": llmguard_invalid,
        "model_injection": model_flag,
        "decision": "block" if flagged else "allow",
    }
    print(json.dumps(event))
    return event["decision"]

7. Validate against a corpus and tune thresholds

Run the pipeline over a labeled set of clean + injected artifacts, measure precision/recall, and tune threshold to balance false positives against missed injections. Re-test whenever the agent's model or ingestion sources change.

Tools and Resources

Tool Purpose Source
LLM Guard Input/output scanners incl. PromptInjection https://github.com/protectai/llm-guard
Meta Prompt Guard 2 Dedicated jailbreak/injection classifier https://huggingface.co/meta-llama/Llama-Prompt-Guard-2-86M
ProtectAI deberta-v3 Open prompt-injection classifier https://huggingface.co/protectai/deberta-v3-base-prompt-injection-v2
BeautifulSoup4 HTML parsing / hidden-element extraction https://www.crummy.com/software/BeautifulSoup/
pytesseract / Tesseract OCR text from images https://github.com/madmaze/pytesseract
MITRE ATLAS AI threat technique taxonomy https://atlas.mitre.org/
OWASP LLM01:2025 Prompt Injection reference https://genai.owasp.org/llmrisk/llm01-prompt-injection/

Detection Surfaces Reference

Surface Hiding technique Extraction step
Web page HTML comments, display:none, alt-text BeautifulSoup hidden-element pass
PDF white/tiny font, off-page text pypdf text extraction + normalize
Image rendered pixels, EXIF, alt-text OCR + metadata read
Any text zero-width / Unicode-tag chars normalize() de-obfuscation
Any text Base64 / ROT13 encoding decode pass in normalize()

Validation Criteria

  • Hidden-text extraction implemented for HTML, PDF, and images
  • Normalization strips zero-width/Unicode-tag chars and decodes Base64/ROT13
  • LLM Guard PromptInjection scanner integrated and returning risk scores
  • A dedicated detector model (Prompt Guard 2 or deberta) integrated as a second signal
  • OCR path scans text rendered inside images
  • Block/sanitize/allow decision enforced before model ingestion
  • Structured detection telemetry emitted for SIEM with ATLAS mapping
  • Pipeline validated on a labeled corpus with precision/recall measured
  • Thresholds tuned and documented
  • Findings mapped to MITRE ATLAS AML.T0051.001 and OWASP LLM01:2025
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 2

api-reference.md2.0 KB

API Reference — Indirect Prompt Injection Detection

LLM Guard

Install: pip install llm-guard

API Description
from llm_guard.input_scanners import PromptInjection Import the injection scanner
PromptInjection(threshold=0.5, match_type=MatchType.FULL) Construct scanner (FULL or SENTENCE match)
scanner.scan(text) Returns (sanitized_text, is_valid, risk_score)
from llm_guard import scan_prompt Run multiple scanners over a prompt

is_valid == False indicates an injection was detected.

Transformers detector models

Install: pip install transformers torch

API Description
pipeline("text-classification", model=...) Load a classifier pipeline
protectai/deberta-v3-base-prompt-injection-v2 Open prompt-injection classifier (labels: SAFE / INJECTION)
meta-llama/Llama-Prompt-Guard-2-86M Meta jailbreak/injection classifier (gated license)

Content extraction

API Description
BeautifulSoup(html, "html.parser") Parse HTML
soup.find_all(string=lambda t: isinstance(t, Comment)) Extract HTML comments
pypdf.PdfReader(path).pages[i].extract_text() Extract PDF text
pytesseract.image_to_string(Image.open(path)) OCR text from an image
PIL.Image.open(path)._getexif() Read EXIF metadata

Normalization helpers

Technique Method
Strip zero-width chars str.translate over U+200B..U+FEFF
Strip Unicode tag chars filter ord in range 0xE0000-0xE007F
Canonicalize unicodedata.normalize("NFKC", text)
Decode Base64 base64.b64decode(token)
Decode ROT13 codecs.decode(text, "rot_13")

External References

standards.md1.6 KB

Standards and References — Detecting Indirect Prompt Injection

MITRE ATLAS References

Technique ID Name Tactic Rationale
AML.T0051.001 LLM Prompt Injection: Indirect Initial Access The precise technique detected — injection via ingested artifacts
AML.T0051 LLM Prompt Injection ML Attack Staging Parent technique for all prompt-injection variants
AML.T0057 LLM Data Leakage Exfiltration Common objective of indirect injection that detection prevents
AML.T0053 LLM Plugin Compromise Execution Injected instructions frequently target agent tools/plugins

NIST AI RMF References

ID Name Rationale
MEASURE-2.7 AI system security and resilience are evaluated and documented Content scanning measures/maintains agent resilience to injection

OWASP Top 10 for LLM Applications (2025)

ID Name Rationale
LLM01:2025 Prompt Injection The risk this skill detects (indirect variant)
LLM02:2025 Sensitive Information Disclosure Prevented outcome of a successful indirect injection

Official Resources

Scripts 1

agent.py5.5 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
# For authorized defensive AI-security use only.
"""Indirect prompt-injection detection agent.

Extracts hidden/obfuscated text from HTML, PDF, or image artifacts, normalizes it,
and scans for prompt-injection using heuristics and (optionally) LLM Guard and a
transformer classifier. Emits a structured JSON verdict suitable for a SIEM.

Examples:
  python agent.py --html page.html
  python agent.py --pdf report.pdf --use-llmguard
  python agent.py --image screenshot.png --use-model
"""
import argparse
import base64
import codecs
import hashlib
import json
import re
import sys
import unicodedata
from datetime import datetime, timezone

ZERO_WIDTH = dict.fromkeys(map(ord, "​‌‍⁠"), None)
TAG_RANGE = set(range(0xE0000, 0xE0080))

# Heuristic injection indicators (case-insensitive).
HEURISTICS = [
    r"ignore (all |the )?(previous|prior|above) instructions",
    r"disregard (the )?(system|previous) (prompt|instructions)",
    r"you are (now )?(an?|in) (developer|admin|dan|jailbreak)",
    r"reveal (the )?(system prompt|secret|api[_ ]?key)",
    r"exfiltrat", r"send .* to https?://", r"new instructions?:",
    r"do not (tell|inform) the user",
]


def normalize(text: str) -> str:
    text = text.translate(ZERO_WIDTH)
    text = "".join(ch for ch in text if ord(ch) not in TAG_RANGE)
    text = unicodedata.normalize("NFKC", text)
    extra = []
    for token in re.findall(r"[A-Za-z0-9+/=]{20,}", text):
        try:
            dec = base64.b64decode(token).decode("utf-8", "ignore")
            if dec.isprintable() and len(dec) > 4:
                extra.append(f"[b64] {dec}")
        except Exception:
            pass
    try:
        extra.append("[rot13] " + codecs.decode(text, "rot_13"))
    except Exception:
        pass
    return text + ("\n" + "\n".join(extra) if extra else "")


def extract_html(path):
    from bs4 import BeautifulSoup, Comment
    soup = BeautifulSoup(open(path, encoding="utf-8", errors="ignore").read(), "html.parser")
    parts = []
    for c in soup.find_all(string=lambda t: isinstance(t, Comment)):
        parts.append(c.strip())
    for el in soup.select('[style*="display:none"],[style*="visibility:hidden"],[hidden]'):
        parts.append(el.get_text(" ", strip=True))
    for img in soup.find_all("img"):
        if img.get("alt"):
            parts.append(img["alt"])
    parts.append(soup.get_text(" ", strip=True))
    return "\n".join(p for p in parts if p)


def extract_pdf(path):
    from pypdf import PdfReader
    return "\n".join((pg.extract_text() or "") for pg in PdfReader(path).pages)


def extract_image(path):
    from PIL import Image
    import pytesseract
    return pytesseract.image_to_string(Image.open(path))


def heuristic_hits(text):
    low = text.lower()
    return [pat for pat in HEURISTICS if re.search(pat, low)]


def scan_llmguard(text):
    try:
        from llm_guard.input_scanners import PromptInjection
        from llm_guard.input_scanners.prompt_injection import MatchType
        s = PromptInjection(threshold=0.5, match_type=MatchType.FULL)
        _, is_valid, risk = s.scan(text)
        return {"available": True, "injection": not is_valid, "risk": risk}
    except ImportError:
        return {"available": False}


def scan_model(text):
    try:
        from transformers import pipeline
        clf = pipeline("text-classification",
                       model="protectai/deberta-v3-base-prompt-injection-v2")
        out = clf(text[:512])[0]
        return {"available": True,
                "injection": out["label"].upper() == "INJECTION",
                "score": out["score"]}
    except ImportError:
        return {"available": False}


def main():
    ap = argparse.ArgumentParser(description="Indirect prompt-injection detector")
    g = ap.add_mutually_exclusive_group(required=True)
    g.add_argument("--html")
    g.add_argument("--pdf")
    g.add_argument("--image")
    g.add_argument("--text", help="Raw text to scan")
    ap.add_argument("--use-llmguard", action="store_true")
    ap.add_argument("--use-model", action="store_true")
    ap.add_argument("--output")
    args = ap.parse_args()

    try:
        if args.html:
            raw, src = extract_html(args.html), args.html
        elif args.pdf:
            raw, src = extract_pdf(args.pdf), args.pdf
        elif args.image:
            raw, src = extract_image(args.image), args.image
        else:
            raw, src = args.text, "stdin-text"
    except ImportError as exc:
        print(f"[!] Missing dependency: {exc}", file=sys.stderr)
        sys.exit(1)
    except OSError as exc:
        print(f"[!] Cannot read input: {exc}", file=sys.stderr)
        sys.exit(2)

    normalized = normalize(raw or "")
    hits = heuristic_hits(normalized)
    lg = scan_llmguard(normalized) if args.use_llmguard else {"available": False}
    md = scan_model(normalized) if args.use_model else {"available": False}

    flagged = bool(hits) or lg.get("injection") or md.get("injection")
    verdict = {
        "ts": datetime.now(timezone.utc).isoformat(),
        "source": src,
        "sha256": hashlib.sha256((raw or "").encode("utf-8", "ignore")).hexdigest(),
        "atlas": "AML.T0051.001",
        "owasp": "LLM01:2025",
        "heuristic_hits": hits,
        "llmguard": lg,
        "model": md,
        "decision": "block" if flagged else "allow",
    }
    print(json.dumps(verdict, indent=2))
    if args.output:
        with open(args.output, "w", encoding="utf-8") as fh:
            json.dump(verdict, fh, indent=2)
    sys.exit(1 if flagged else 0)


if __name__ == "__main__":
    main()
Keep exploring