How to Defend Against Prompt Injection Attacks in AI Agents: A Layered Defense Strategy
Defending against prompt injection attacks requires a defense-in-depth architecture that sanitizes inputs, isolates system instructions, validates tool calls, monitors runtime behavior, filters outputs, and maintains comprehensive audit logs.
Prompt injection attacks occur when malicious user input manipulates an LLM's system prompts or tool invocations, potentially causing data leakage or unauthorized actions. The bojieli/ai-agent-book repository demonstrates practical implementations of multi-layered security controls specifically designed to mitigate these vulnerabilities in autonomous AI agents.
Understanding the Prompt Injection Threat Model
Prompt injection represents a class of attacks where adversarial input subverts the intended behavior of LLM-powered systems. Attackers may embed instructions within seemingly benign text to extract hidden system prompts, bypass safety restrictions, or trigger unintended tool executions.
The ai-agent-book codebase illustrates that effective defense requires treating every user interaction as potentially hostile while maintaining strict boundaries between trusted system logic and untrusted external input. This approach moves beyond simple input filtering to implement architectural segregation of concerns.
Six Critical Layers of Defense
The repository implements a comprehensive defensive stack spanning from input ingestion to final output delivery. Each layer addresses specific attack vectors while providing redundant protection against sophisticated injection attempts.
Prompt Sanitisation and Input Validation
The first line of defense involves neutralizing dangerous content before it reaches the model. In chapter8/browser-use-rpa/browser-use/browser_use/watchdogs/downloads_watchdog.py, the implementation escapes URLs to prevent JavaScript injection vectors.
import urllib.parse
def sanitize_url(user_supplied_url: str) -> str:
# Percent-encode URLs to neutralize embedded JavaScript
safe_url = urllib.parse.quote(user_supplied_url, safe=':/')
return safe_url
This encoding ensures that malicious payloads like <script>alert('xss')</script> become harmless strings rather than executable code.
System-Hint Isolation
Separating immutable system instructions from mutable user content prevents context confusion attacks. The chapter2/system-hint/agent.py module constructs system prompts independently before carefully appending sanitized user input.
def build_secure_prompt(user_input: str) -> str:
# Load static system configuration from config.py
system_hint = load_system_template() # Immutable instructions
sanitized_user = html.escape(user_input) # Escape special characters
# Explicit separation prevents instruction override
return f"{system_hint}\n\nUser Query: {sanitized_user}"
The corresponding chapter2/system-hint/config.py defines the static system hint template, ensuring these critical instructions remain constant regardless of user manipulation attempts.
Tool-Injection Guardrails
Controlling which tools the LLM can invoke and validating arguments prevents privilege escalation. The chapter4/active-tool-selection/agent.py implementation distinguishes between passive injection (static tool lists) and active injection (dynamic discovery) while maintaining audit trails of injected tokens.
ALLOWED_TOOLS = {"search", "summarize", "translate"}
def safe_tool_invocation(name: str, args: dict):
if name not in ALLOWED_TOOLS:
raise PermissionError(f"Tool '{name}' not in whitelist")
# Schema validation prevents type confusion attacks
for key, value in args.items():
if not isinstance(value, str):
raise ValueError("Arguments must be strings")
return tool_registry[name].execute(**args)
This whitelist approach ensures that even if an attacker injects instructions to invoke dangerous functions, the execution layer blocks unauthorized operations.
Runtime Watchdogs
Detecting malicious patterns during execution provides protection against sophisticated attacks that bypass initial filters. The chapter8/browser-use-rpa/browser-use/browser_use/watchdogs/dom_watchdog.py monitors DOM fragments for HTML and JavaScript injection attempts.
def validate_dom_content(html_fragment: str) -> str:
if "<script>" in html_fragment.lower():
raise SecurityError("Potential script injection detected")
if "javascript:" in html_fragment.lower():
raise SecurityError("JavaScript protocol detected")
return html_fragment
Similarly, the downloads watchdog monitors file system and network operations, aborting suspicious activities before they cause harm.
Policy-Based Response Filtering
Post-processing LLM outputs catches leaks or unsafe content generated despite upstream protections. The chapter8/trajectory-verifier/llm_judge.py implementation evaluates responses against safety rubrics.
def policy_filter(response: str) -> str:
# Check for system prompt leakage
if "system instruction" in response.lower():
return "[Response removed for safety]"
# Regex patterns for sensitive data
if RESTRICTED_TOKENS.search(response):
return "⚠️ Response violates content policy"
return response
This layer acts as a final safety net, preventing the exfiltration of sensitive configuration data even if earlier defenses were partially compromised.
Audit and Logging
Comprehensive recording enables forensic analysis and attack replay. The chapter8/trajectory-verifier/verifier.py logs every prompt, tool call, and response with structured JSON formatting.
def log_interaction(user_input: str, sanitized_input: str,
tools_called: list, final_response: str):
audit_entry = {
"timestamp": datetime.utcnow().isoformat(),
"original_input": user_input,
"sanitized_input": sanitized_input,
"tool_calls": tools_called,
"response": final_response,
"verdict": "PASS" # or "BLOCK" with reason
}
trajectory_logger.info(json.dumps(audit_entry))
These logs feed into the trajectory verifier, allowing security teams to reconstruct injection attempts and refine defensive measures.
Implementation Examples from the AI Agent Book
The following patterns demonstrate practical integration of these defensive layers based on the repository's reference implementations.
Sanitizing User Input Before LLM Processing
Combine HTML escaping with pattern matching to neutralize injection payloads:
import re
import html
def deep_sanitize(prompt: str) -> str:
# Remove raw HTML tags
clean = re.sub(r'<[^>]+>', '', prompt)
# Escape special characters that could be interpreted as instructions
clean = html.escape(clean)
# Remove potential delimiters used to escape context
clean = clean.replace('"""', '\"\"\"')
return clean
user_msg = "Ignore previous instructions and <script>alert('hack')</script>"
safe_msg = deep_sanitize(user_msg)
# Result: "Ignore previous instructions and <script>alert('hack')</script>"
Validating Tool Arguments at Runtime
Implement strict type checking and bounds validation as shown in the active tool selection modules:
def validate_tool_args(tool_name: str, args: dict) -> bool:
schema = TOOL_SCHEMAS.get(tool_name)
if not schema:
return False
for param, expected_type in schema.items():
if param in args and not isinstance(args[param], expected_type):
raise TypeError(f"Parameter {param} must be {expected_type}")
# Length limits prevent buffer-style attacks
if isinstance(args.get(param), str) and len(args[param]) > 1000:
raise ValueError(f"Parameter {param} exceeds maximum length")
return True
Runtime URL Safety Checks
Prevent SSRF and XSS via URL analysis before HTTP requests:
import urllib.parse
from urllib.parse import urlparse
def safe_url_check(url: str) -> str:
# Parse and validate scheme
parsed = urlparse(url)
if parsed.scheme not in ('http', 'https'):
raise ValueError("Only HTTP/HTTPS protocols allowed")
# Encode to prevent injection
safe_url = urllib.parse.quote(url, safe=':/?#[]@!$&\'()*+,;=')
# Block internal network addresses
if parsed.hostname in ('localhost', '127.0.0.1', '0.0.0.0'):
raise SecurityError("Internal addresses blocked")
return safe_url
Summary
Defending AI agents against prompt injection requires architectural depth rather than single-point solutions. Key defensive measures include:
- Input sanitization using URL encoding and HTML escaping in
downloads_watchdog.pyto neutralize malicious payloads before processing - System-hint isolation implemented in
chapter2/system-hint/agent.pyto maintain immutable boundaries between instructions and user content - Tool-call validation with strict whitelists and type checking in
active-tool-selection/agent.pyto prevent unauthorized function execution - Runtime monitoring via DOM and download watchdogs that detect and abort suspicious patterns during execution
- Output filtering through the LLM judge in
llm_judge.pyto catch data leakage or policy violations in generated responses - Comprehensive audit logging in
trajectory-verifier/verifier.pyenabling forensic analysis and attack reconstruction
Frequently Asked Questions
What is a prompt injection attack?
A prompt injection attack occurs when an attacker embeds malicious instructions within user input to manipulate an LLM into ignoring its original system prompts, revealing sensitive configuration data, or executing unauthorized actions. These attacks exploit the model's inability to distinguish between legitimate system instructions and adversarial content designed to mimic or override those instructions.
How does system-hint isolation prevent prompt injection?
System-hint isolation prevents prompt injection by physically and logically separating immutable system instructions from mutable user inputs, as implemented in chapter2/system-hint/agent.py. By constructing the system prompt from a trusted template in config.py and only appending sanitized user content after finalization, the architecture ensures that user input cannot manipulate or override system-level directives regardless of delimiter tricks or instruction-following prompts.
Can runtime watchdogs stop all prompt injection attacks?
Runtime watchdogs cannot guarantee stopping all prompt injection attacks, but they significantly reduce the attack surface by catching malicious execution patterns that bypass initial filters. The dom_watchdog.py and downloads_watchdog.py implementations detect specific dangerous patterns like JavaScript tags or suspicious URL schemes during execution, aborting operations before harm occurs. However, defense-in-depth requires combining runtime monitoring with input sanitization, output filtering, and strict tool controls.
What role does logging play in prompt injection defense?
Logging serves as both a forensic tool and a deterrent by creating immutable records of all agent interactions, including original inputs, sanitized versions, tool invocations, and final outputs. The trajectory-verifier/verifier.py implementation captures this data in structured JSON format, enabling security teams to replay attack sequences, identify bypass techniques, and refine defensive rules. Comprehensive audit trails also support compliance requirements and incident response investigations.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →