Free lesson · GenAI Security Engineering
Implement input sanitization pipeline with NeMo Guardrails
Configure NeMo Guardrails Colang flows for instruction-data separation, context boundary validation, and system prompt protection.
Course: AI Security Engineering · Chapter 1 · Prompt Injection Defense
Free to read — no subscription required.
Introduction
When users submit text to an LLM-powered application, that text and the system's own instructions share the same prompt — giving an attacker a chance to embed rogue directives inside what should be treated as passive data. Enforcing a strict boundary between instructions and data requires more than ad-hoc checks scattered through application code; it demands a structured, declarative approach that can be updated without redeploying the service. This lesson teaches how to define Colang input flows in NeMo Guardrails that intercept every user message, detect instruction-like patterns and delimiter attacks, and block or sanitize violations before they reach the LLM backend.
Key Terminology
- Colang input flow — A named, declarative sequence registered under
rails.input.flowsinrails_configthat NeMo Guardrails evaluates against every incoming user message before the message is forwarded to the LLM backend. - Instruction-data separation — The security principle that user-submitted text (data) and the system's directives (instructions) must be treated as structurally distinct, even though both arrive as plain text in the same prompt window.
- Delimiter manipulation — An attack technique in which an adversary embeds delimiter characters (such as triple backticks) inside user input to break the context boundary and cause the model to interpret user data as instructions.
- BoundaryViolation — A dataclass produced by
validate_context_boundariesthat records theviolation_type,SeverityLevel, matched pattern text, characterposition, and a 50-charactercontext_windowfor each detected boundary breach. - SeverityLevel — An enum with values
HIGH,MEDIUM, andLOWthat classifies the danger of a detectedBoundaryViolation; aHIGHresult causes the Colang action to block the message and return a safe refusal instead of calling the LLM backend. - Short-circuit ordering — The flow-sequencing strategy where a fast, pattern-based flow (such as
check_instruction_patterns) is listed before a slower flow inrails.input.flowsso that cheap detections exit the pipeline early without invoking the LLM backend.
Concepts
The Instruction-Data Boundary Problem
Every prompt that reaches an LLM is a single, flat string of text. The model distinguishes system instructions from user data only by convention — a system role prefix or a structural position in the prompt — not by any mechanism the model enforces at inference time. An attacker who controls user input can therefore craft text that looks, to the model, like it continues or overrides the system instructions. Embedding rogue directives inside what should be passive data is the core mechanic of prompt injection, and the model has no reliable way to refuse.
This is why enforcement must happen before the combined prompt is assembled. Once user input and system instructions are merged into a single string and handed to the LLM backend, the opportunity to separate them has passed. Instruction-data separation must be a pre-processing gate, not an expectation placed on the model itself.
Colang Flows as a Declarative Interception Layer
NeMo Guardrails addresses this by providing Colang flows — named, declarative sequences registered in rails_config under rails.input.flows. The runtime intercepts every user message and evaluates listed flows in order before any call to the LiteLLM backend (see Code Walkthrough). Because flows are defined in configuration rather than baked into application code, they can be updated, reordered, or extended without redeploying the service — a meaningful operational advantage when new attack patterns emerge.
The two flows in this lesson — check_instruction_patterns and check_boundary_violations — illustrate a deliberate ordering strategy. A flow that scans for known keyword signatures is cheap to run; a flow that calls validate_context_boundaries and iterates compiled regex rules costs more per message. Placing the cheaper flow first means any message that triggers a keyword-level match never reaches the second flow — a short-circuit that keeps the hot path fast without sacrificing coverage.
Severity-Driven Blocking
Detection without a structured response is incomplete. The validate_context_boundaries function returns a list of BoundaryViolation objects, each carrying a SeverityLevel. This separation of concerns is intentional: the detector produces evidence; the Colang action reads that evidence and decides what to do. A HIGH-severity violation causes the action to mark the flow as blocked, and the Guardrails runtime returns a safe refusal without forwarding the input to the LLM backend. Violations at lower severity levels may be logged or passed through, depending on the policy the action implements.
Each BoundaryViolation also captures the character position and a context_window of 50 characters around the match — not for blocking logic, but for observability. When a pattern fires unexpectedly on legitimate input, the context window reveals exactly what triggered it, making false-positive tuning tractable rather than speculative.
Code Walkthrough
Building on the instruction-data separation principle from the Concepts section, the examples below show how to wire Colang input rails into NeMo Guardrails and enforce context boundaries in Python.
The first step is registering the flows as input rails in the Guardrails configuration. The rails_config dictionary declares two flows — check_instruction_patterns and check_boundary_violations — that the runtime evaluates in order before each user message reaches the Gemini 2.0 Flash backend via LiteLLM. Ordering matters: a keyword-level flow runs first and short-circuits before a more expensive semantic flow is invoked, keeping the hot path fast.
Code snippetpython
1# Colang flow definition for instruction-data separation 2rails_config = { 3 "models": [{"type": "main", "engine": "litellm", "model": "gemini/gemini-2.0-flash"}], 4 "rails": { 5 "input": { 6 "flows": ["check_instruction_patterns", "check_boundary_violations"] 7 } 8 } 9}
The check_boundary_violations flow calls a Python action that scans the raw input for delimiter manipulation and prompt-leakage patterns. The validator below is fully self-contained: it defines SeverityLevel and BoundaryRule using only standard-library primitives, then iterates compiled regex patterns over the input to produce a list of BoundaryViolation objects — each carrying the violation type, severity, matched text, character position, and a 50-character context window for debugging.
Code snippetpython
1import re 2from dataclasses import dataclass 3from enum import Enum 4 5class SeverityLevel(str, Enum): 6 HIGH = "high" 7 MEDIUM = "medium" 8 LOW = "low" 9 10@dataclass 11class BoundaryRule: 12 name: str 13 compiled_pattern: re.Pattern 14 severity: SeverityLevel 15 16@dataclass 17class BoundaryViolation: 18 violation_type: str 19 severity: SeverityLevel 20 matched_pattern: str 21 position: int 22 context_window: str 23 24def validate_context_boundaries( 25 user_input: str, 26 boundary_rules: list[BoundaryRule], 27) -> list[BoundaryViolation]: 28 violations = [] 29 for rule in boundary_rules: 30 for match in rule.compiled_pattern.finditer(user_input): 31 violations.append(BoundaryViolation( 32 violation_type=rule.name, 33 severity=rule.severity, 34 matched_pattern=match.group(), 35 position=match.start(), 36 context_window=user_input[max(0, match.start()-50):match.end()+50], 37 )) 38 return violations
When validate_context_boundaries returns a BoundaryViolation with severity=SeverityLevel.HIGH, the Colang action marks the flow as blocked and the Guardrails runtime returns a safe refusal response instead of forwarding the input to the LLM backend. Inputs that produce no violations pass through to the system prompt protection rail, which guards against extraction attempts — completing the layered defense described in the Concepts section.
Confirm that calling validate_context_boundaries with a triple-backtick delimiter injection string returns at least one BoundaryViolation and that the rails_config runtime blocks the message before a LiteLLM call is made.
Do's and Don'ts
Now that you have worked through the implementation, the practices below separate a durable approach from a fragile one.
Do's
- ✓Do declare both
check_instruction_patternsandcheck_boundary_violationsas ordered entries inrails.input.flows— the Guardrails runtime evaluates flows sequentially, so placing the cheaper keyword-matching flow first short-circuits before the more expensive semantic flow is invoked, keeping the hot path fast without sacrificing coverage. - ✓Do compile regex patterns into
BoundaryRule.compiled_patternonce at module load time, not insidevalidate_context_boundaries— re-compiling on every call multiplies latency proportionally to input volume; pre-compiledre.Patternobjects are thread-safe and reused across the lifetime of the Guardrails runtime. - ✓Do capture
positionandcontext_windowon everyBoundaryViolation— the 50-character window around each match gives defenders the exact offset and surrounding text needed to triage new delimiter-injection patterns without re-running the full input through the validator.
Don'ts
- ✗Don't place
check_boundary_violationsbeforecheck_instruction_patternsin theflowslist — inverting the order forces every message through the heavier boundary-scan logic even when a simple keyword match would have blocked the input first, defeating the short-circuit design that makes the rail production-safe. - ✗Don't return a bare boolean from
validate_context_boundariesinstead of a list ofBoundaryViolationobjects — a boolean tells the Colang action only that a violation occurred, discarding theseverity,violation_type, andmatched_patternfields that determine whether the runtime should block withSeverityLevel.HIGHor pass through to the next rail with a lower-severity finding. - ✗Don't scatter ad-hoc
re.searchchecks across application code outside theBoundaryRule/validate_context_boundariespipeline — inline checks bypass the Guardrails input rail entirely, meaning violations are evaluated after the message has already been forwarded toward the LiteLLM/Gemini 2.0 Flash backend rather than intercepted before it.
This lesson is free to read. Its 3 hands-on labs — real code, in a cloud IDE — are part of the GenAI Security Engineering subscription.
From · cancel anytime · Already a subscriber? Sign in →
Listen to this lesson
Audio overviews of this lesson's labs and its chapter, from GenBodha Bytes.
- Prompt Injection DefenseChapter overview20 min
More free lessons in AI Security Engineering
- Ch 1Build prompt injection classifier using LLM-as-judge via LiteLLM
- Ch 1Implement input sanitization pipeline with NeMo GuardrailsYou are here
- Ch 1Detect indirect injection in RAG-retrieved documents
- Ch 1Build defense-in-depth with layered guard chain
- Ch 1Deploy injection defense as FastAPI sidecar on GKE
- Ch 1Monitor injection attempts with Prometheus and Grafana
- Ch 3Deploy output sanitizer as response middleware on GKE