All skills
semgrep avatar

/llm-security

@327da93 official
by semgrepsemgrep/skills317 stars
31

Security guidelines for LLM applications based on OWASP Top 10 for LLM 2025. Use when building LLM apps, reviewing AI security, implementing RAG systems, or asking about LLM vulnerabilities like 'prompt injection' or 'check LLM security'. IMPORTANT: Always consult this skill when building chatbots, AI agents, RAG pipelines, tool-using LLMs, agentic systems, or any application that calls an LLM API (OpenAI, Anthropic, Gemini, etc.) — even if the user doesn't explicitly mention security. Also use when users import 'openai', 'anthropic', 'langchain', 'llamaindex', or similar LLM libraries.

Use this Skill: https://skilld.dev/gh/semgrep/skills/llm-security

This session only. Nothing lands on disk.

rulesoutput-handling.md

≈2.5k tokens on demand. Your agent reads this file only when SKILL.md points to it.

LLM05: Secure Output Handling

Improper output handling occurs when LLM-generated content is passed to downstream systems without adequate validation and sanitization. Since LLM outputs can be influenced by user prompts (including malicious ones), treating them as trusted input creates injection vulnerabilities.

Key principle: Treat all LLM output as untrusted user input that requires validation before use.


Preventing XSS from LLM Output

Vulnerable (direct HTML rendering):

// DANGEROUS: Direct injection of LLM response into HTML
async function displayResponse(userQuery) {
  const response = await llm.generate(userQuery);
  document.getElementById('output').innerHTML = response; // XSS vulnerability
}

Secure (proper encoding):

import DOMPurify from 'dompurify';

async function displayResponse(userQuery) {
  const response = await llm.generate(userQuery);

  // Option 1: Sanitize HTML
  const sanitized = DOMPurify.sanitize(response, {
    ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li'],
    ALLOWED_ATTR: []
  });
  document.getElementById('output').innerHTML = sanitized;

  // Option 2: Use textContent for plain text (safest)
  document.getElementById('output').textContent = response;
}
# Python/Flask example
from markupsafe import escape
from flask import render_template

@app.route('/chat')
def chat():
    response = llm.generate(request.args.get('query'))

    # Escape HTML entities
    safe_response = escape(response)

    return render_template('chat.html', response=safe_response)

Preventing SQL Injection from LLM Output

Vulnerable (LLM generates SQL):

def query_database(user_request: str) -> list:
    # LLM generates SQL based on user request
    sql_query = llm.generate(f"Generate SQL for: {user_request}")

    # DANGEROUS: Direct execution of LLM-generated SQL
    cursor.execute(sql_query)
    return cursor.fetchall()

Secure (parameterized queries with validation):

import re
from typing import Optional

ALLOWED_TABLES = ["products", "categories", "orders"]
ALLOWED_COLUMNS = {
    "products": ["id", "name", "price", "description"],
    "categories": ["id", "name"],
    "orders": ["id", "product_id", "quantity", "status"]
}

def validate_sql_components(table: str, columns: list[str], conditions: dict) -> bool:
    """Validate SQL components against allowlist."""
    if table not in ALLOWED_TABLES:
        return False

    for col in columns:
        if col not in ALLOWED_COLUMNS.get(table, []):
            return False

    # Validate condition columns
    for col in conditions.keys():
        if col not in ALLOWED_COLUMNS.get(table, []):
            return False

    return True

def safe_query_database(user_request: str) -> list:
    # LLM extracts structured query components (not raw SQL)
    query_components = llm.generate(
        f"""Extract query components from this request as JSON:
        {user_request}

        Return format: {{"table": "...", "columns": [...], "conditions": {{...}}}}
        Only use tables: {ALLOWED_TABLES}"""
    )

    components = json.loads(query_components)

    # Validate components
    if not validate_sql_components(
        components["table"],
        components["columns"],
        components.get("conditions", {})
    ):
        raise ValueError("Invalid query components")

    # Build parameterized query
    columns = ", ".join(components["columns"])
    table = components["table"]
    conditions = components.get("conditions", {})

    if conditions:
        where_clause = " AND ".join(f"{k} = %s" for k in conditions.keys())
        sql = f"SELECT {columns} FROM {table} WHERE {where_clause}"
        params = list(conditions.values())
    else:
        sql = f"SELECT {columns} FROM {table}"
        params = []

    cursor.execute(sql, params)
    return cursor.fetchall()

Preventing Command Injection from LLM Output

Vulnerable (LLM generates shell commands):

import subprocess

def execute_task(user_request: str):
    # LLM generates command based on user request
    command = llm.generate(f"Generate shell command for: {user_request}")

    # DANGEROUS: Direct shell execution
    subprocess.run(command, shell=True)

Secure (restricted command execution):

import subprocess
import shlex
from typing import Optional

ALLOWED_COMMANDS = {
    "list_files": ["ls", "-la"],
    "disk_usage": ["df", "-h"],
    "current_dir": ["pwd"],
    "date": ["date"],
}

def execute_task(user_request: str) -> str:
    # LLM selects from predefined commands (not generates)
    command_selection = llm.generate(
        f"""Select the appropriate command for this request: {user_request}
        Available commands: {list(ALLOWED_COMMANDS.keys())}
        Return only the command name."""
    )

    command_name = command_selection.strip().lower()

    if command_name not in ALLOWED_COMMANDS:
        raise ValueError(f"Command not allowed: {command_name}")

    # Execute predefined command (no user input in command)
    result = subprocess.run(
        ALLOWED_COMMANDS[command_name],
        capture_output=True,
        text=True,
        timeout=30,
        shell=False  # Never use shell=True with LLM output
    )

    return result.stdout

# For commands that need parameters, use strict validation
def execute_with_params(command_name: str, params: dict) -> str:
    """Execute command with validated parameters."""

    PARAM_VALIDATORS = {
        "list_directory": {
            "path": lambda p: p.startswith("/home/") and ".." not in p
        }
    }

    if command_name not in PARAM_VALIDATORS:
        raise ValueError("Unknown command")

    # Validate each parameter
    for param_name, value in params.items():
        validator = PARAM_VALIDATORS[command_name].get(param_name)
        if not validator or not validator(value):
            raise ValueError(f"Invalid parameter: {param_name}")

    # Build command safely
    if command_name == "list_directory":
        return subprocess.run(
            ["ls", "-la", params["path"]],
            capture_output=True,
            text=True,
            shell=False
        ).stdout

Preventing SSRF from LLM Output

Vulnerable (LLM provides URLs):

import requests

def fetch_url(user_request: str) -> str:
    # LLM extracts or generates URL
    url = llm.generate(f"Extract the URL from: {user_request}")

    # DANGEROUS: Fetching arbitrary URLs
    response = requests.get(url)
    return response.text

Secure (URL validation and allowlisting):

import requests
from urllib.parse import urlparse
import ipaddress

ALLOWED_DOMAINS = ["api.example.com", "docs.example.com"]
BLOCKED_IP_RANGES = [
    ipaddress.ip_network("10.0.0.0/8"),
    ipaddress.ip_network("172.16.0.0/12"),
    ipaddress.ip_network("192.168.0.0/16"),
    ipaddress.ip_network("127.0.0.0/8"),
    ipaddress.ip_network("169.254.0.0/16"),
]

def is_safe_url(url: str) -> bool:
    """Validate URL is safe to fetch."""
    try:
        parsed = urlparse(url)

        # Must be HTTPS
        if parsed.scheme != "https":
            return False

        # Check domain allowlist
        if parsed.hostname not in ALLOWED_DOMAINS:
            return False

        # Resolve and check IP
        import socket
        ip = socket.gethostbyname(parsed.hostname)
        ip_addr = ipaddress.ip_address(ip)

        for blocked_range in BLOCKED_IP_RANGES:
            if ip_addr in blocked_range:
                return False

        return True

    except Exception:
        return False

def fetch_url(user_request: str) -> str:
    url = llm.generate(f"Extract the URL from: {user_request}")
    url = url.strip()

    if not is_safe_url(url):
        raise ValueError(f"URL not allowed: {url}")

    response = requests.get(
        url,
        timeout=10,
        allow_redirects=False  # Prevent redirect-based bypass
    )
    return response.text

Content Security Policy for LLM Applications

Implementation:

from flask import Flask, make_response

app = Flask(__name__)

@app.after_request
def add_security_headers(response):
    # Strict CSP to mitigate XSS from LLM output
    response.headers['Content-Security-Policy'] = (
        "default-src 'self'; "
        "script-src 'self'; "  # No inline scripts
        "style-src 'self' 'unsafe-inline'; "
        "img-src 'self' data:; "
        "connect-src 'self' https://api.openai.com; "
        "frame-ancestors 'none'; "
        "form-action 'self';"
    )
    response.headers['X-Content-Type-Options'] = 'nosniff'
    response.headers['X-Frame-Options'] = 'DENY'
    return response

Key Prevention Rules

  1. Treat LLM output as untrusted - Apply same validation as user input
  2. Encode for context - HTML-encode for web, parameterize for SQL
  3. Use allowlists - Restrict outputs to predefined safe values
  4. Never use shell=True - Avoid shell execution with LLM-derived input
  5. Validate URLs - Check domains and prevent internal network access
  6. Apply CSP - Use Content Security Policy to limit damage from XSS
  7. Log and monitor - Track LLM outputs that trigger validation failures

References:

Source: SKILL.md on GitHub

1 alert16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The skill provides comprehensive security guidelines and code examples for building secure LLM applications, based on the OWASP Top 10 for LLMs 2025. It serves as an educational resource to help developers mitigate risks like prompt injection and sensitive data exposure. While the skill contains examples of vulnerable code with hardcoded credentials, these are clearly labeled as insecure patterns for demonstration and educational purposes, using non-functional example values.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: MEDIUM · 1 issue

  • Runlayer6mo

    12/14 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 327da93. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Steadyupdated 7 months ago
  • llm-security
  • prompt-injection
  • owasp
  • rag
  • ai-agents
  • data-poisoning
  • sensitive-disclosure
  • output-handling
  • vector-embeddings

README badge

README badge for semgrep/skills/llm-security

Provides security guidelines for LLM applications based on OWASP Top 10 for LLM 2025, covering prompt injection, sensitive disclosure, data poisoning, excessive agency, and other LLM-specific risks. Use when building chatbots, RAG systems, AI agents, or any application calling LLM APIs to identify relevant vulnerabilities and apply secure patterns.

Generated from the current SKILL.md.

Does this skill cover prompt injection attacks?
Yes. Prompt Injection (LLM01) is a critical category with dedicated rules for preventing both direct and indirect prompt manipulation in chatbots, RAG systems, and tool-using LLMs.
What LLM APIs and libraries does this apply to?
This skill applies to any application calling OpenAI, Anthropic, Gemini, or similar LLM APIs, and to code using LangChain, LlamaIndex, or comparable LLM frameworks.
Should I use this skill only when the user explicitly asks about security?
No. The skill is designed for proactive use: automatically check for relevant security risks whenever building or reviewing LLM applications, chatbots, RAG pipelines, or AI agents — regardless of whether the user mentions security.
Does this cover RAG system security?
Yes. RAG systems have priority rules for Vector/Embedding Weaknesses (LLM08), Prompt Injection (LLM01), Sensitive Disclosure (LLM02), and Misinformation (LLM09).
Are there code examples for each vulnerability?
Yes. Each of the 10 OWASP categories has a dedicated rule file in `rules/` with vulnerable and secure code examples.

Generated from the current SKILL.md. These answers refresh after source changes.