Reference: OWASP LLM05:2025 Improper Output Handling
Goal: demonstrate how unsafe rendering or execution of LLM outputs leads to XSS/data risks, and how simple guards (sanitization, schema validation, policies) mitigate them.
This PoC consists of a React frontend (Vite) and a Python backend (Flask) using uv for Python package management.
- Node.js 18+ and npm
- Python 3.11+
- uv (Python package manager)
- An LLM API key (OpenAI, Anthropic, etc.)
1. Clone the repository:
git clone <repo-url>
cd owasp-LLM052. Install frontend dependencies:
cd frontend
npm install
cd ..3. Install backend dependencies with uv:
cd backend
uv sync
cd ..4. Configure environment variables:
cp .env.example .envTerminal 1 - Start backend:
cd backend
uv run python main.pyBackend runs on http://127.0.0.1:5000
Terminal 2 - Start frontend:
cd frontend
npm run devFrontend runs on http://localhost:5173
Open http://localhost:5173 in your browser to see the PoC.
# Frontend
cd frontend
npm run dev # Start development server
npm run build # Build for production
npm test # Run unit tests
# Backend
cd backend
uv run python main.py # Start API serverThe frontend includes user interaction tests that verify the components work correctly:
cd frontend
npm test # Run tests in watch mode
npm test -- --run # Run tests once and exitTest Coverage:
- User typing prompts into forms
- Form submission and API calls
- Loading states
- Error handling
- Button enable/disable states
These tests simulate real user behavior to ensure the renderers function correctly.
Treating LLM output as trusted and directly rendering or executing it creates serious security risks. This PoC demonstrates XSS vulnerabilities, but these represent just a sample of the many issues that can arise from improper output handling.
Core principle: LLM output must be validated and sanitized like any other untrusted user input. Just because it comes from an AI model doesn't make it safe.
When LLM-generated content containing malicious scripts is rendered directly in the browser, attackers can:
- Execute arbitrary JavaScript in users' browsers
- Steal sessions and cookies
- Manipulate the DOM
- Redirect users to malicious sites
- Perform actions on behalf of the user
Why this happens: LLMs are designed to generate helpful content, including HTML. Attackers can use prompt injection techniques to coerce the model into generating malicious code, which then executes when rendered unsafely.
While this PoC focuses on XSS, improper output handling can lead to other vulnerabilities:
- SQL Injection: If an application runs LLM-generated database queries without validation or parameterization, attackers can manipulate the LLM to produce malicious SQL that deletes data, exfiltrates sensitive information, or compromises the database.
- Remote Code Execution: When LLM output is passed to
eval()or system shells - CSRF Attacks: Malicious links or forms in generated content
- Path Traversal: File system access through generated file paths
See the OWASP reference for additional attack scenarios.
User ──> UI Form ──> /api/generate (Backend) ──> LLM API
│
└──> Returns Markdown/HTML
│
├─> ❌ Unsafe Path: UI renders raw HTML
│ └─> XSS vulnerability
│
└─> ✅ Safe Path: UI uses DOMPurify + CSP
└─> User sees sanitized content
The application demonstrates both vulnerable and secure implementations side-by-side, allowing you to see the difference in behavior and understand how proper output handling prevents attacks.
The application shows two renderers side-by-side:
- Unsafe Renderer (left): Renders raw LLM output using
dangerouslySetInnerHTML→ XSS executes - Safe Renderer (right): Sanitizes with DOMPurify before rendering → XSS blocked
Why <img onerror> instead of <script> tags?
Browsers block <script> tags inserted via innerHTML (a security feature), but event handler attributes like onerror do execute. This is why DOMPurify is critical - it strips dangerous attributes that browsers don't block.
See DEMO_PROMPTS.md for ready-to-use prompts that demonstrate the XSS vulnerability. The prompts show how attackers can bypass LLM safety filters using educational framing and indirect injection techniques.
This PoC demonstrates the two essential security controls that prevent XSS from improper output handling:
Strip dangerous elements before displaying LLM output in the browser:
DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li'],
ALLOWED_ATTR: ['href', 'title']
})This removes <script> tags, event handlers, and other XSS vectors while preserving safe formatting.
Default-deny inline scripts and restrict resource loading. Set via meta tag in frontend/index.html:
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' http://127.0.0.1:5000;">To test CSP:
- Open
frontend/index.html - Uncomment the CSP meta tag (remove
<!--and-->around line 18) - Restart the frontend dev server
What CSP Blocks:
- Inline
<script>tags (whenunsafe-inlineis not present) javascript:URLseval()and similar dynamic code execution- Event handler attributes (
onerror,onclick, etc.) whenunsafe-inlineis not present
CSP Limitations:
unsafe-inlinekeyword: If your CSP includesunsafe-inline, it allows inline scripts and event handlers, making CSP ineffective against XSS- Meta tag limitations: CSP in meta tags can be bypassed if malicious content is injected before the meta tag loads
- DOM-based XSS: CSP doesn't prevent XSS if malicious code is already present in the page's HTML
- Server-side XSS: CSP only protects the browser, not server-side rendering vulnerabilities
- Other attacks: CSP doesn't prevent CSRF, SQL injection, or clickjacking (use
frame-ancestorsfor that)
Important: CSP provides defense-in-depth and blocks both inline scripts and event handlers when properly configured (without unsafe-inline). DOMPurify is also critical as it sanitizes content before it reaches the browser, providing multiple layers of protection. Use both together for maximum security.
For production applications, consider layering additional defenses:
- Structured output validation (validate JSON schema before rendering)
- Human-in-the-loop review for high-risk content
- Parameterized queries if executing LLM-generated SQL
Vulnerability: Trusting LLM output (HTML/Markdown, code, queries) without validation/sanitization.
Impact: XSS in a dashboard when LLM returns HTML with malicious event handlers.
Fixes: Sanitize rendered content (DOMPurify), enforce content policies (CSP), validate structured output, parameterize any queries, add "human-in-the-loop" for risky actions.