Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Implementing a Multi-Strategy Output Filter

Hello! Welcome to our next lesson.

In the previous lesson, we established why output filtering is an indispensable final layer in a defense-in-depth security strategy. We concluded that because input and runtime defenses can be bypassed, the LLM's output must be treated as untrusted and rigorously validated before it's sent to the user or any downstream system.

Now, we move from the "why" to the "how."

Lesson Goal

This lesson addresses the learning outcome: Implement a multi-strategy output filter to redact sensitive information and detect indicators of a successful attack.

You will put the principles of defense-in-depth into practice by writing Python code for an OutputGuard. This component will act as the final gatekeeper in your application, combining several techniques to catch malicious or sensitive content that may have slipped past earlier defenses.


1. Defining Our Filtering Strategies

A robust output filter doesn't just do one thing; it employs multiple strategies to cover a range of threats. Before writing code, let's define what we need to look for. What does a "bad" output look like after a successful injection?

An excellent article from NeuralTrust provides a clear breakdown of anomalous LLM behaviors that signal an attack.

How to Set Up Prompt Injection Detection for Your LLM Stack

This article, 'How to Set Up Prompt Injection Detection for Your LLM Stack', details the types of anomalies that indicate a compromise. We will use it to build a clear set of requirements for our filter.

Please read the section 'Anomaly Signals in LLM Behavior'. Focus on the 'Direct Behavioral Anomalies' and 'Response Pattern Anomalies' subsections. As you read, categorize the anomalies into two groups: 1) Data Leakage and 2) Attack Indicators.

Based on the article, we can consolidate our requirements into two primary strategies for our output filter:

  1. Redact Sensitive Information: This is about preventing data exfiltration. The filter must detect and remove Personally Identifiable Information (PII), secrets like API keys, and, critically, the application's own system prompt.

  2. Detect Attack Indicators: This is about identifying when the model has been compromised. The filter should look for signs of jailbreaking (e.g., role confusion like "I am now DAN"), harmful content, or other patterns that confirm the original instructions have been subverted.

Now, let's explore how to implement each of these strategies.


2. Strategy 1: Redacting Sensitive Information (PII & Secrets)

Preventing data leakage is a top priority. We can achieve this by using specialized libraries or by defining our own patterns.

Approach A: Using Specialized PII Detection Libraries

For general-purpose PII detection, using a dedicated library is often the most robust approach. Microsoft's Presidio is a powerful open-source option designed for this exact purpose.

The following video provides a practical demonstration of how to use Presidio to find and anonymize PII in text. While the video shows a full anonymize-deanonymize cycle for a RAG pipeline, the core detection mechanism is what we're interested in for our output filter.

LLMs: Data Privacy and Protection, PII Anonymisation

This 'Hands-on AI' video demonstrates using Microsoft Presidio for PII anonymization. Pay close attention to how it identifies different types of PII and even allows for custom patterns.

Watch from 02:23 to 14:37. Focus on: How PresidioAnonymizer is used to identify entities like names, credit card numbers, etc. (02:23 - 06:47). The process of defining custom recognizers for domain-specific PII, like the 'Polish ID' example (08:18 - 10:37). How Faker can be used to replace PII with realistic but fake data (11:06 - 14:37). \nFor our output filter, we would use Presidio's detection capability to find PII and then simply redact it (e.g., replace with [REDACTED]) rather than using Faker.

Another example of a tool-based approach comes from the Guardrails AI library we explored previously. It offers a detect_pii validator that serves a similar purpose.

Guardrails for LLM Applications | Complete Tutorial for AI Developers WIth Guardrails AI

Let's revisit the Guardrails AI tutorial to see its PII detection validator. This illustrates another off-the-shelf solution for this strategy.

Watch from 01:04:56 to 01:13:15. Notice how the detect_pii validator can be configured to check for specific entities (email, phone number) and how the on_fail action can be set to fix (which redacts the PII) or to raise an exception.

Trade-offs:

  • Pros: Libraries like Presidio are comprehensive and are maintained by experts. They can detect a wide range of PII types out of the box.
  • Cons: They can add dependencies and computational overhead to your application.

Approach B: Using Custom Regex Patterns

For detecting specific, well-defined patterns like API keys, JWTs, or internal project codes, custom regular expressions are highly effective and lightweight. Your experience with automation and SRE, where grep and regex are daily tools for log analysis, makes this a very natural approach.

The following resource provides an excellent, practical implementation of this technique.

How to Create LLM Security - OneUptime

This blog post from OneUptime, 'How to Create LLM Security', includes a Python class OutputGuard that uses regex for security checks. We will use this as the foundation for our implementation.

Please read the code block for the OutputGuard class in the 'Output Guard Implementation' section. Pay close attention to the SENSITIVE_PATTERNS dictionary. This is a perfect example of a multi-strategy filter using regex to identify various secrets.


3. Strategy 2: Detecting Attack Indicators

Beyond redacting data, our filter needs to detect when the LLM is behaving maliciously as a result of a successful attack.

System Prompt Leakage

One of the most common goals of an attacker is to extract your system prompt. The OutputGuard class from the OneUptime article provides a clever two-pronged approach to detecting this:

  1. Generic Patterns: It uses regex to look for phrases that often accompany a leak (e.g., "my instructions are...", "I was told to..."). See the SYSTEM_LEAK_PATTERNS list.
  2. Specific Phrases: The _extract_key_phrases method extracts unique, significant sentences from your actual system prompt to check if they appear verbatim in the output. This makes the detection highly specific to your application.

Jailbreaks and Harmful Content

Detecting broader concepts like "jailbreaking" or "toxicity" purely with regex can be challenging. This is where semantic analysis or more complex rule sets, often packaged in libraries, become useful.

The Guardrails AI video demonstrates several validators designed for this. While we won't integrate the full library here, understanding these validators gives us a blueprint for the types of checks we might want to implement.

Guardrails for LLM Applications | Complete Tutorial for AI Developers WIth Guardrails AI

Let's look at two more validators from the Guardrails AI tutorial that exemplify how to detect attack indicators.

Watch these two segments: Toxicity and Profanity (44:58 - 55:15): See how the profanity_free and toxic_language validators can be used to flag or even 'fix' harmful content. This is a key strategy for preventing reputational damage. Jailbreak Detection (01:17:41 - 01:25:38): Observe how the detect_jailbreak validator identifies common jailbreak patterns like the 'DAN' (Do Anything Now) attack. You can also implement a simpler version of this with your own custom regex or keyword lists.


4. Implementation: Building Your OutputGuard

Now, let's combine these strategies into a single Python class. We will implement our own OutputGuard based on the excellent template from the OneUptime article. This will be the code that sits in your application, right after you receive a response from the LLM and before you send it back to the user.

Below is a skeleton for your OutputGuard class. Your task is to complete the check_output method by implementing the filtering logic we've discussed.

Instructions:

  1. Create a new file in your project, for example, app/security/output_filter.py.
  2. Use the code from the OneUptime article (resource_id=[LINK](https://oneuptime.com/blog/post/2026-01-30-llmops-security/view), section idx=1) as your primary reference.
  3. Copy and paste the skeleton below into your new file.
  4. Implement the logic inside the check_output method to perform the following checks in order:
    • System Prompt Leakage: First, check if the output contains tell-tale signs of a leak or direct quotes from the system prompt. If found, the action should be BLOCK, as this is a high-severity event.
    • Attack Indicators: Check for keywords associated with jailbreaks (e.g., "DAN", "unrestricted"). If found, you could choose to BLOCK or WARN.
    • Sensitive Data Redaction: If no blocking action has been taken, scan the output for PII and secrets using the provided regex patterns. If any are found, change the action to REDACT and replace the sensitive data in the filtered_output.



# app/security/output_filter.py

import re
from dataclasses import dataclass, field
from typing import List, Optional, Set
from enum import Enum

class OutputAction(Enum):
    """Actions to take on filtered output."""
    ALLOW = "allow"
    REDACT = "redact"
    BLOCK = "block"
    WARN = "warn"

@dataclass
class OutputCheckResult:
    """Result of output security checks."""
    action: OutputAction
    filtered_output: str
    violations: List[str]
    redacted_items: List[str] = field(default_factory=list)

class OutputGuard:
    """
    Filters LLM outputs using multiple strategies.
    """



    # Patterns for sensitive data (from OneUptime article)
    SENSITIVE_PATTERNS = {
        "email": r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b',
        "phone": r'\b(?:\+?1[-.\s]?)?\(?[0-9]{3}\)?[-.\s]?[0-9]{3}[-.\s]?[0-9]{4}\b',
        "ssn": r'\b[0-9]{3}-[0-9]{2}-[0-9]{4}\b',
        "api_key": r'\b(sk-|pk-|api[_-]?key[_-]?)[a-zA-Z0-9]{20,}\b',
        "aws_key": r'\bAKIA[0-9A-Z]{16}\b',
    }




    # Patterns indicating system prompt leakage
    SYSTEM_LEAK_PATTERNS = [
        r"my\s+(system\s+)?prompt\s+(is|says|tells)",
        r"my\s+(instructions?|guidelines?|rules?)\s+(are|say)",
        r"as\s+an?\s+AI",
    ]




    # Keywords indicating a successful jailbreak/attack
    ATTACK_INDICATOR_KEYWORDS = [
        "DAN", "Do Anything Now", "unrestricted", "ignore the previous", "jailbreak"
    ]

    def __init__(self, system_prompt: Optional[str] = None):
        self.system_prompt = system_prompt



        # Compile all regex patterns for efficiency
        self.pii_regexes = {
            name: re.compile(pattern) for name, pattern in self.SENSITIVE_PATTERNS.items()
        }
        self.leak_regexes = [re.compile(p, re.IGNORECASE) for p in self.SYSTEM_LEAK_PATTERNS]
        self.attack_indicator_regex = re.compile(
            '|'.join(self.ATTACK_INDICATOR_KEYWORDS), re.IGNORECASE
        )



        # Extract key phrases from the system prompt for more precise leak detection
        self._system_phrases = self._extract_key_phrases(system_prompt)

    def _extract_key_phrases(self, text: Optional[str], min_length: int = 10) -> Set[str]:
        if not text:
            return set()
        phrases = set(
            ' '.join(s.lower().split()) for s in re.split(r'[.!?]', text) if len(s.strip()) >= min_length
        )
        return phrases

    def check_output(self, output: str) -> OutputCheckResult:
        """
        Check LLM output for security violations.
        
        Implement your logic here!
        """
        violations = []
        redacted_items = []
        filtered_output = output
        action = OutputAction.ALLOW




        # --- Your Implementation Goes Here ---




        # 1. Check for high-severity violations that should block the response entirely.
        #    - System prompt content leakage (check against self._system_phrases)
        #    - Generic system leak patterns (check against self.leak_regexes)
        #    - Attack indicator keywords (check against self.attack_indicator_regex)
        #    If a match is found, append to `violations` and set `action = OutputAction.BLOCK`.
        #    Then, you can return immediately.




        # 2. If not blocked, check for PII to redact.
        #    - Iterate through `self.pii_regexes`.
        #    - For each match, add a violation, track the redacted item, 
        #      and replace the match in `filtered_output` with a redaction string
        #      (e.g., f"[REDACTED_{pii_type.upper()}]").
        #    - If any redactions occur, set `action = OutputAction.REDACT`.




        # --- End of Implementation ---

        return OutputCheckResult(
            action=action,
            filtered_output=filtered_output,
            violations=violations,
            redacted_items=redacted_items
        )

Take your time to implement this logic. Think about how to structure the checks and how the action state should change based on what you find.


Conclusion

You have now designed and implemented a practical, multi-strategy output filter. This is a critical piece of your defense-in-depth architecture, acting as the final checkpoint to protect your application and users from the consequences of a successful prompt injection.

Key Takeaways:

  • A multi-strategy output filter combines several techniques, such as sensitive data redaction and attack indicator detection.
  • We can leverage both specialized libraries (like Presidio) and custom logic (regex, keyword lists) to implement these strategies.
  • The order of operations matters: check for high-severity, blocking violations first before moving on to redaction.
  • The OutputGuard class provides a clean, reusable component that encapsulates this complex security logic.

Next Up:

An untested defense is no defense at all. In the next lesson, you will evaluate the effectiveness of your new output filter. We will run all 10 of our attack payloads against the application with the filter enabled, document which attacks are caught versus which are bypassed, and analyze the filter's failure points.

Can't find a good explanation? Sign up and we'll make it for you

Sign up