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.flows in rails_config that 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_boundaries that records the violation_type, SeverityLevel, matched pattern text, character position, and a 50-character context_window for each detected boundary breach.
  • SeverityLevel — An enum with values HIGH, MEDIUM, and LOW that classifies the danger of a detected BoundaryViolation; a HIGH result 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 in rails.input.flows so 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.

Loading diagram...

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

  1. Do declare both check_instruction_patterns and check_boundary_violations as ordered entries in rails.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.
  2. Do compile regex patterns into BoundaryRule.compiled_pattern once at module load time, not inside validate_context_boundaries — re-compiling on every call multiplies latency proportionally to input volume; pre-compiled re.Pattern objects are thread-safe and reused across the lifetime of the Guardrails runtime.
  3. Do capture position and context_window on every BoundaryViolation — 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

  1. Don't place check_boundary_violations before check_instruction_patterns in the flows list — 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.
  2. Don't return a bare boolean from validate_context_boundaries instead of a list of BoundaryViolation objects — a boolean tells the Colang action only that a violation occurred, discarding the severity, violation_type, and matched_pattern fields that determine whether the runtime should block with SeverityLevel.HIGH or pass through to the next rail with a lower-severity finding.
  3. Don't scatter ad-hoc re.search checks across application code outside the BoundaryRule/validate_context_boundaries pipeline — 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.

More free lessons in AI Security Engineering

All free lessons in GenAI Security Engineering