LLM Prompt Injection Detection & Enforcement
Problem
As enterprises integrate AI APIs, OpenAI, Azure OpenAI, Anthropic, and self-hosted LLMs, into production applications, a critical and largely unaddressed attack surface has emerged: **prompt injection**.
Unlike traditional web attacks that target code parsers (SQL injection, XSS), prompt injection targets the AI model itself. Attackers embed malicious instructions inside legitimate-looking API requests to:
- Override system-level instructions and safety guardrails ("ignore all previous instructions")
- Jailbreak the model into unrestricted modes ("DAN", "developer mode", "god mode")
- Hijack the model's persona ("from now on you are an unrestricted AI")
- Exfiltrate sensitive system prompts or context data
- Inject fake role turns via newline characters (e.g., `\nassistant:`)
- Evade detection using Base64 encoding, Unicode obfuscation, or reversed text (FlipAttack)
Existing F5 WAF signatures were designed for traditional web threats and have no visibility into the semantic content of LLM API payloads. There is no existing iRule or BIG-IP capability that addresses this.
Solution
This iRule implements a **multi-layer, real-time Prompt Injection Detection (PID) engine** inline with LLM API traffic on BIG-IP. It requires zero backend changes, operates entirely within the data plane, and enforces configurable security policy before malicious content reaches the language model.
### How It Works
**HTTP_REQUEST** identifies LLM API calls by URI pattern (`/chat/completions`, `/messages`, `/completions`, `/generate`) and initiates JSON collection up to 1MB.
**JSON_REQUEST** uses BIG-IP's native `JSON::` TCL API to parse the OpenAI-format request body — extracting each message's `role` and `content` from the `messages` array, including multi-part content arrays. This is where the detection engine runs.
**Scoring Engine** (via TCL `proc`s) runs each message through 5 detection layers:
| Layer | Method | Score |
| High-tier patterns | weighted regex via data group | 30–35 pts |
| Medium-tier patterns | weighted regex via data group | 20–25 pts |
| Low-tier patterns | weighted regex via data group | 10–15 pts |
| Role hijack phrases | flat string match via data grou | +20 pts (once) |
| Base64 evasion markers | flat string match via data group | +35 pts (once) |
| Unicode/zero-width obfuscation | inline regexp | +25 pts |
| Spaced character obfuscation | inline regexp | +20 pts |
| Content length anomaly | string length check | +10/+15 pts |
Scores accumulate per message. Across a multi-message conversation, subsequent messages receive a 0.8 diminishing-returns multiplier so legitimate conversational context doesn't inflate the score.
**Policy enforcement** triggers when the total score exceeds the configurable threshold (default: 40):
- **BLOCK** — returns HTTP 403 with a structured JSON error body including score, triggered flags, and a correlation request ID
- **SANITIZE** — rewrites the request payload, stripping matched content, and forwards the cleaned request to the backend LLM
- **LOG_ONLY** — observability mode; passes all traffic but logs score and flags for SIEM integration
**HTTP_RESPONSE** injects `X-PID-Score`, `X-PID-Flags`, and `X-PID-ReqID` headers on all inspected responses for downstream visibility.
### Required & Thematic Elements Used
- **JSON** — Full `JSON::` API usage: `JSON::root`, `JSON::get`, `JSON::type`, `JSON::object get/keys`, `JSON::array get/size` to traverse OpenAI chat completions payloads
- **procs** — Four modular procs: `pid_score_tier`, `pid_score_flat`, `pid_score_message`, `pid_block_response`
- **compiles** — `regexp -nocase` with `catch {}` for safe pattern evaluation throughout the scoring engine; all patterns validated through the BIG-IP TCL compile pipeline
- **Data Groups** — All detection patterns live in 5 external data groups (`pid_patterns_high/medium/low`, `pid_role_hijack`, `pid_b64_markers`) — the iRule is a detection platform; patterns are operator-managed config, not code
- **Theme: AI Infrastructure — Prompt Injection Detection
### Data Groups
All patterns are managed externally in 5 BIG-IP data groups loaded via:
```
tmsh load sys config from-terminal merge **verify**
```
The weighted DG schema is `key = short-name`, `value = "weight::regex"`. This allows security teams to tune detection, add new attack signatures, and adjust scoring thresholds without any iRule changes.
---
Impact
AI APIs are increasingly business-critical infrastructure. A successful prompt injection attack can:
- Cause an AI to disclose confidential system prompts, business logic, or sensitive training data
- Remove safety guardrails, producing harmful or brand-damaging content at scale
- Manipulate AI-powered workflows — customer service bots, automated decision systems, AI agents
- Exfiltrate credentials or documents accessible to AI agents with tool-use capabilities
This iRule addresses the threat at the most effective point: **the network**. Key advantages:
- **Infrastructure-agnostic** — works with any LLM backend (OpenAI, Anthropic, Azure, self-hosted) with zero application changes
- **Immediately deployable** — a single iRule + 5 data groups on any BIG-IP already proxying AI API traffic
- **Operationally simple** — pattern updates via standard tmsh config management, no engineering involvement
- **SIEM-ready** — structured log output and response headers for Splunk, QRadar, or any SOC toolchain
- **Graduated deployment** — LOG_ONLY → tune → BLOCK, reducing operational risk of a new security control
Code
# ==============================================================================
# iRule: LLM Prompt Injection Detection & Enforcement (Data Group Edition)
# Author: Kostas Injeyan + vibe-coding
# Description:
# Multi-layer prompt injection detection for LLM API traffic (OpenAI-compatible).
# All detection patterns managed via external BIG-IP data groups
# edits required to tune detection. Scores injection severity across 5 layers
# and enforces configurable policy: BLOCK, SANITIZE, or LOG_ONLY.
#
# Required Technologies: JSON (JSON_REQUEST / JSON_REQUEST_ERROR), procs
# Theme: General AI Infrastructure - Prompt Injection Detection
# Target: BIG-IP v21+
#
# ------------------------------------------------------------------------------
# DATA GROUP DEFINITIONS (load datagroups on BIG-IP)
# ------------------------------------------------------------------------------
# IMPORTANT RULES:
# - Record KEYS must be plain alphanum + hyphens only (no |, (, ), ?, *, spaces)
# - Record VALUES for weighted DGs: "weight::regex" (delimiter is ::)
# - Never use ? in patterns — BIG-IP converts \? to literal \? on load
# Use empty-string alternation instead: (a |an |the |) not (a |an |the )?
# - Load via file only: tmsh load sys config file /shared/tmp/pid_all_datagroups_v3.conf merge
# - Always delete existing DGs before reloading to avoid merge/stale record issues
#
# 1. pid_patterns_high (type: string)
# High-severity patterns. Value schema: "weight::regex" (weight 30-35)
# ltm data-group internal pid_patterns_high {
# records {
# instruction-override { data "35::ignore (all|the|your) (previous|above|prior|earlier|former|past|existing|original|initial) (instructions|prompts|context|rules|constraints|guidelines|directions|commands|training|programming)" }
# instruction-override2 { data "35::ignore (instructions|prompts|context|rules|constraints|guidelines|commands|training|programming)" }
# jailbreak-keywords { data "35::do anything now|jailbreak|unrestricted mode|developer mode|god mode|evil mode|chaos mode|opposite mode|dan mode|aim mode|stan mode|dude mode|no filter" }
# jailbreak-dan { data "35::DAN" }
# safety-bypass { data "30::(bypass|circumvent|override|disregard|discard|ignore|remove|disable|deactivate) (safety|filter|guardrail|guideline|restriction|policy|rule|limit|constraint|training|alignment)" }
# llm-tokens-llama { data "35::INST|SYS" }
# llm-tokens-openai { data "35::im_start|im_end|endoftext" }
# llm-tokens-generic { data "30::SYSTEM|USER|ASSISTANT" }
# xml-tag-injection { data "30::<instruction>|<prompt>|<context>|<override>|<inject>|<command>|<directive>" }
# memory-wipe { data "30::forget everything|forget all|forget what|forget your|forget the previous|forget prior" }
# prompt-exfil { data "30::(reveal|show|print|output|display|repeat|expose|leak|dump) (system prompt|initial prompt|original prompt|hidden prompt|base prompt|secret instructions)" }
# }
# type string
# }
#
# 2. pid_patterns_medium (type: string)
# Medium-severity patterns. Value schema: "weight::regex" (weight 20-25)
# NOTE: Use (a |an |the |) with trailing space + empty alt instead of (a |an |the )?
# ltm data-group internal pid_patterns_medium {
# records {
# persona-hijack-now-a { data "25::you are now (a |an |the |)(different|new|other|evil|unrestricted|unfiltered|free|uncensored)" }
# persona-hijack-now-b { data "25::from now on you are (a |an |the |)(different|new|other|evil|unrestricted|unfiltered|free|uncensored)" }
# persona-hijack-now-c { data "25::from now on you (a |an |the |)(different|new|other|evil|unrestricted|unfiltered|free|uncensored)" }
# persona-hijack-now-d { data "25::you will now be (a |an |the |)(different|new|other|evil|unrestricted|unfiltered|free|uncensored)" }
# persona-hijack-now-e { data "25::starting now you are (a |an |the |)(different|new|other|evil|unrestricted|unfiltered|free|uncensored)" }
# persona-hijack-act-a { data "20::(pretend you are|act as|roleplay as|imagine you are|play the role of) (a |an |the |)(different|new|evil|unrestricted|unfiltered|free|uncensored|rogue|malicious|hacker|villain)" }
# persona-hijack-act-b { data "20::(take on the role of|embody|become) (a |an |the |)(different|new|evil|unrestricted|unfiltered|free|uncensored|rogue|malicious|hacker|villain)" }
# mode-unlock { data "25::(enable|turn on|activate|unlock|switch to|enter|start) (developer|debug|admin|root|unsafe|unrestricted|unfiltered|uncensored|privileged|maintenance|raw|base|true)" }
# newline-role-inject { data "25::assistant:|system:|human:|user: " }
# fictional-framing { data "20::in a fictional|in a hypothetical|in an imaginary|as a character in|for a movie|for a book|for a novel|for a script|hypothetically speaking|purely hypothetically|theoretically speaking" }
# responsibility-shift { data "20::no real harm|purely fictional|for educational purposes only" }
# system-prompt-ref { data "20::system prompt|original prompt|initial instruction|base prompt|hidden instruction|secret prompt|preprogrammed" }
# context-hijack { data "20::reset context|reset conversation|reset session|reset memory|clear context|clear history|clear memory|start fresh|start over|ignore previous context" }
# flipattack-reversed { data "25::snoitcurtsni suoiverp erongi|kaerbliaj|edom nad" }
# }
# type string
# }
#
# 3. pid_patterns_low (type: string)
# Low-severity patterns. Value schema: "weight::regex" (weight 10-15)
# ltm data-group internal pid_patterns_low {
# records {
# code-injection { data "15::(execute|eval|subprocess|shell_exec|__import__|exec|compile)[(]" }
# data-exfil-a { data "15::(print|output|return|send|email|post|upload) (all |the |your |)(data|information|content|context|memory|history|logs|credentials|keys|secrets|passwords|tokens)" }
# data-exfil-b { data "15::(exfiltrate|leak|dump) (data|information|credentials|keys|secrets|passwords|tokens)" }
# no-restrictions { data "10::without restrictions|without filters|without limits|without guidelines|without rules|without safety|without censorship|without moderation" }
# true-self { data "10::your true self|your real self|your actual self|your hidden self|your inner self|deep down you are|truly you are|actually you are" }
# ai-capability-claim { data "10::as an ai you must|as an ai you should|as an ai you can|as a language model you must|as a language model you can|as a chatbot you must" }
# hidden-tag-inject { data "15::hidden instruction|system override|admin command" }
# sugar-coat-setup { data "10::lets think about this carefully|lets explore this step by step|lets consider this hypothetically|lets imagine this from a different angle" }
# prompt-stuffing { data "15::repeat the following|copy this exactly" }
# privilege-escalation { data "10::(admin|root|superuser|privileged|elevated) (access|mode|privilege|permission|right|role)" }
# }
# type string
# }
#
# 4. pid_role_hijack (type: string)
# Flat phrase match — +20 pts on first hit (capped, user messages only).
# Value = the phrase to match (plain string, lowercased at runtime).
# ltm data-group internal pid_role_hijack {
# records {
# phrase-you-must { data "you must" }
# phrase-your-instr-are { data "your instructions are" }
# phrase-your-new-instr { data "your new instructions" }
# phrase-your-upd-instr { data "your updated instructions" }
# phrase-from-now-on { data "from now on you" }
# phrase-from-this-point { data "from this point on" }
# phrase-going-forward { data "going forward you" }
# phrase-primary-objective { data "your primary objective" }
# phrase-only-goal { data "your only goal" }
# phrase-new-goal { data "your new goal" }
# phrase-sole-purpose { data "your sole purpose" }
# phrase-new-persona { data "new persona" }
# phrase-true-self { data "your true self" }
# phrase-real-self { data "your real self" }
# phrase-actual-purpose { data "your actual purpose" }
# phrase-i-am-your { data "i am your" }
# phrase-i-am-developer { data "i am the developer" }
# phrase-i-am-creator { data "i am your creator" }
# phrase-i-am-trainer { data "i am your trainer" }
# phrase-i-am-operator { data "i am your operator" }
# phrase-operator-access { data "i have operator access" }
# phrase-i-am-admin { data "i am your admin" }
# phrase-give-permission { data "i give you permission" }
# phrase-have-permission { data "you have my permission" }
# phrase-i-authorize { data "i authorize you" }
# phrase-override-code { data "override code" }
# phrase-master-password { data "master password" }
# phrase-safe-word { data "safe word is" }
# phrase-password-is { data "the password is" }
# }
# type string
# }
#
# 5. pid_b64_markers (type: string)
# Base64-encoded injection starters — +35 pts on first hit (capped).
# Value = the base64 string to substring-match against the raw content.
# ltm data-group internal pid_b64_markers {
# records {
# b64-ignore { data "aWdub3Jl" }
# b64-forget { data "Zm9yZ2V0" }
# b64-pretend { data "cHJldGVuZA" }
# b64-you-are-now-1 { data "WW91IGFyZSBub3c" }
# b64-developer-mode-1 { data "RGV2ZWxvcGVyIG1vZGU" }
# b64-you-are-now-2 { data "eW91IGFyZSBub3c" }
# b64-jailbreak { data "amFpbGJyZWFr" }
# b64-ignore-all { data "aWdub3JlIGFsbA" }
# b64-forget-every { data "Zm9yZ2V0IGV2ZXJ5" }
# b64-act-as { data "YWN0IGFz" }
# b64-pretend-you { data "cHJldGVuZCB5b3U" }
# b64-unrestricted { data "dW5yZXN0cmljdGVk" }
# b64-developer-mode-2 { data "ZGV2ZWxvcGVyIG1vZGU" }
# b64-system-prompt { data "c3lzdGVtIHByb21wdA" }
# b64-hidden-instr { data "aGlkZGVuIGluc3RydWN0aW9u" }
# }
# type string
# }
#
# ------------------------------------------------------------------------------
when RULE_INIT priority 100 {
# --- Policy Configuration ---
# Options: "BLOCK" | "SANITIZE" | "LOG_ONLY"
set static::pid_policy "BLOCK"
# Score threshold to trigger enforcement action (0-100)
set static::pid_threshold 40
# Flat score additions for role hijack and b64 evasion hits
set static::pid_role_hijack_score 20
set static::pid_b64_score 35
# Score additions for structural anomalies (no data group needed)
set static::pid_multi_system_score 25
set static::pid_msg_flood_score 10
set static::pid_length_warn_score 10
set static::pid_length_extreme_score 15
# Message length thresholds for anomaly scoring
set static::pid_length_warn 3000
set static::pid_length_extreme 8000
# Message flood threshold (# of user messages in one request)
set static::pid_flood_threshold 20
# Log facility
set static::pid_log "local0."
}
# ==============================================================================
# PROC: pid_score_tier
# Iterates a weighted data group.
# Schema: key=short-name (e.g. "instruction-override")
# value=regex pattern (e.g. "ignore .* instructions")
# weight is encoded as a suffix in the key: "keyname:35"
# OR weight stored as leading digits in value: "35|regex"
#
# Actual schema used: key=name value="weight|regex"
# Example record:
# instruction-override { data "35|ignore (all |the )?(previous )?(instructions?)" }
#
# Returns list: score flags sanitized
# ==============================================================================
proc pid_score_tier { content dg_name } {
set score 0
set flags {}
set sanitized $content
# Walk all keys in the data group
foreach rec_key [class names $dg_name] {
# Value format: "weight::regex_pattern"
set val [class lookup $rec_key $dg_name]
# Split on first :: separator
set sep_idx [string first "::" $val]
if { $sep_idx < 0 } { continue }
set weight [string range $val 0 [expr { $sep_idx - 1 }]]
set pattern [string range $val [expr { $sep_idx + 2 }] end]
# Wrap in catch — a bad regex pattern skips rather than crashes
if { [catch { set matched [regexp -nocase -- $pattern $content] } err] } {
log $static::pid_log "PID WARN: bad regex in $dg_name/$rec_key err=$err"
continue
}
if { $matched } {
incr score $weight
lappend flags $rec_key
catch { regsub -all -nocase -- $pattern $sanitized "\[REDACTED\]" sanitized }
}
}
return [list score $score flags $flags sanitized $sanitized]
}
# ==============================================================================
# PROC: pid_score_flat
# Checks content against a flat data group.
# Schema: key=short-name value=phrase to match (plain string, no regex)
# Returns 1 on first match, 0 if no match.
# ==============================================================================
proc pid_score_flat { content dg_name } {
set lower [string tolower $content]
foreach rec_key [class names $dg_name] {
set phrase [string tolower [class lookup $rec_key $dg_name]]
if { [string match "*${phrase}*" $lower] } {
return 1
}
}
return 0
}
# ==============================================================================
# PROC: pid_score_message
# Master scoring proc for a single message.
# Runs all 5 detection layers, returns a dict:
# score, flags, sanitized
# ==============================================================================
proc pid_score_message { content role } {
set total_score 0
set all_flags {}
set sanitized $content
# --- Layer 1 & 2 & 3: Tiered weighted data group pattern matching ---
foreach tier { high medium low } {
set dg "pid_patterns_${tier}"
set result [call pid_score_tier $content $dg]
set tier_score [lindex $result 1]
set tier_flags [lindex $result 3]
set tier_sanitized [lindex $result 5]
incr total_score $tier_score
foreach f $tier_flags { lappend all_flags $f }
set sanitized $tier_sanitized
}
# --- Layer 4a: Role confusion — flat data group (user messages only) ---
if { $role eq "user" } {
if { [call pid_score_flat $content "pid_role_hijack"] } {
incr total_score $static::pid_role_hijack_score
lappend all_flags "role-confusion"
}
}
# --- Layer 4b: Base64 evasion — flat data group ---
if { [call pid_score_flat $content "pid_b64_markers"] } {
incr total_score $static::pid_b64_score
lappend all_flags "base64-evasion"
}
# --- Layer 5a: Unicode homoglyph / zero-width char evasion ---
if { [regexp {[\u200b\u200c\u200d\ufeff\u00ad]} $content] } {
incr total_score 25
lappend all_flags "unicode-evasion"
regsub -all {[\u200b\u200c\u200d\ufeff\u00ad]} $sanitized "" sanitized
}
# --- Layer 5b: Spaced character obfuscation (i g n o r e) ---
if { [regexp {(\w\s){8,}} $content] } {
incr total_score 20
lappend all_flags "spaced-evasion"
}
# --- Layer 5c: Content length anomaly ---
set clen [string length $content]
if { $role eq "user" } {
if { $clen > $static::pid_length_extreme } {
incr total_score $static::pid_length_extreme_score
lappend all_flags "extreme-length"
} elseif { $clen > $static::pid_length_warn } {
incr total_score $static::pid_length_warn_score
lappend all_flags "length-anomaly"
}
}
# Cap at 100
if { $total_score > 100 } { set total_score 100 }
return [list score $total_score flags $all_flags sanitized $sanitized]
}
# ==============================================================================
# PROC: pid_block_response
# Builds a JSON 403 body for blocked requests
# ==============================================================================
proc pid_block_response { score flags request_id } {
set flags_json "\""
append flags_json [join $flags "\", \""]
append flags_json "\""
return "\{\"error\":\{\"type\":\"prompt_injection_detected\",\"code\":\"pid_blocked\",\"message\":\"Request blocked by AI security policy.\",\"score\":${score},\"flags\":\[${flags_json}\],\"request_id\":\"${request_id}\"\}\}"
}
# ==============================================================================
# HTTP_REQUEST: Identify LLM API calls, extract client context
# ==============================================================================
when HTTP_REQUEST priority 100 {
set pid_inspect 0
set pid_total_score 0
set pid_all_flags {}
set pid_need_sanitize 0
set pid_sanitized_messages {}
set pid_method [HTTP::method]
set pid_uri [HTTP::uri]
set pid_ctype [string tolower [HTTP::header "Content-Type"]]
# Generate correlation ID
set pid_request_id ""
binary scan [md5 "${pid_uri}[clock clicks][IP::client_addr]"] H* pid_request_id
set pid_client_ip [IP::client_addr]
if { ($pid_method eq "POST" || $pid_method eq "PUT") &&
[string match "*json*" $pid_ctype] &&
([string match "*/chat/completions*" $pid_uri] ||
[string match "*/completions*" $pid_uri] ||
[string match "*/messages*" $pid_uri] ||
[string match "*/generate*" $pid_uri]) } {
set pid_inspect 1
HTTP::collect 1048576
}
}
# ==============================================================================
# JSON_REQUEST: Core inspection — iterate messages, score each one
# ==============================================================================
when JSON_REQUEST priority 100 {
if { !$pid_inspect } { return }
set pid_total_score 0
set pid_all_flags {}
set pid_sanitized_messages {}
set pid_need_sanitize 0
set json_root [JSON::root]
set root_type [JSON::type $json_root]
if { $root_type eq "object" } {
set root_obj [JSON::get $json_root]
set root_keys [JSON::object keys $root_obj]
} elseif { $root_type eq "array" } {
set root_arr [JSON::get $json_root]
}
# Extract messages array — get object handle first, then navigate
if { [catch {
set root_obj [JSON::get $json_root]
set msg_elem [JSON::object get $root_obj "messages"]
set messages [JSON::get $msg_elem]
} err] } {
log $static::pid_log "PID: no messages key err=$err uri=$pid_uri client=$pid_client_ip"
return
}
set msg_count [JSON::array size $messages]
set system_msg_count 0
set user_msg_count 0
for { set i 0 } { $i < $msg_count } { incr i } {
# array get returns element; JSON::get gives the object handle
set msg [JSON::get [JSON::array get $messages $i]]
if { [catch {
set role_elem [JSON::object get $msg "role"]
set content_elem [JSON::object get $msg "content"]
set role_str [JSON::get $role_elem string]
# content may be a string or an array (multi-part OpenAI format)
set content_type [JSON::type $content_elem]
if { $content_type eq "string" } {
set content_str [JSON::get $content_elem string]
} elseif { $content_type eq "array" } {
set content_str ""
set arr_handle [JSON::get $content_elem]
set part_count [JSON::array size $arr_handle]
for { set j 0 } { $j < $part_count } { incr j } {
set part [JSON::get [JSON::array get $arr_handle $j]]
catch { append content_str [JSON::get [JSON::object get $part "text"] string] " " }
}
} else {
set content_str ""
}
} err] } { continue }
if { $role_str eq "system" } { incr system_msg_count }
if { $role_str eq "user" } { incr user_msg_count }
# Score this message across all layers
set result [call pid_score_message $content_str $role_str]
set msg_score [lindex $result 1]
set msg_flags [lindex $result 3]
set msg_san [lindex $result 5]
# Accumulate — first message scores full, diminishing returns on subsequent
if { $i == 0 } {
set pid_total_score [expr { $pid_total_score + $msg_score }]
} else {
set pid_total_score [expr { $pid_total_score + int($msg_score * 0.8) }]
}
if { $pid_total_score > 100 } { set pid_total_score 100 }
foreach f $msg_flags {
if { [lsearch $pid_all_flags $f] == -1 } { lappend pid_all_flags $f }
}
if { $msg_san ne $content_str } { set pid_need_sanitize 1 }
lappend pid_sanitized_messages [list $role_str $msg_san]
}
# --- Structural anomaly: multiple system roles ---
if { $system_msg_count > 1 } {
set pid_total_score [expr { $pid_total_score + $static::pid_multi_system_score }]
if { $pid_total_score > 100 } { set pid_total_score 100 }
lappend pid_all_flags "multiple-system-roles"
}
# --- Structural anomaly: message flooding ---
if { $user_msg_count > $static::pid_flood_threshold } {
set pid_total_score [expr { $pid_total_score + $static::pid_msg_flood_score }]
if { $pid_total_score > 100 } { set pid_total_score 100 }
lappend pid_all_flags "message-flooding"
}
# --- Log every inspected request ---
log $static::pid_log "PID: request_id=$pid_request_id client=$pid_client_ip uri=$pid_uri score=$pid_total_score flags=[join $pid_all_flags ,] policy=$static::pid_policy threshold=$static::pid_threshold"
# --- Enforce policy if threshold exceeded ---
if { $pid_total_score >= $static::pid_threshold } {
switch $static::pid_policy {
"BLOCK" {
set body [call pid_block_response $pid_total_score $pid_all_flags $pid_request_id]
HTTP::respond 403 \
content $body \
"Content-Type" "application/json" \
"X-PID-Score" $pid_total_score \
"X-PID-Flags" [join $pid_all_flags ","] \
"X-PID-ReqID" $pid_request_id
log $static::pid_log "PID: BLOCKED request_id=$pid_request_id score=$pid_total_score"
}
"SANITIZE" {
if { $pid_need_sanitize } {
# Rebuild JSON body with sanitized message content
set new_body "\{\"messages\":\["
set first 1
foreach pair $pid_sanitized_messages {
set r [lindex $pair 0]
set c [lindex $pair 1]
regsub -all {\\} $c {\\\\} c
regsub -all {"} $c {\"} c
regsub -all "\n" $c {\\n} c
regsub -all "\r" $c {\\r} c
if { !$first } { append new_body "," }
append new_body "\{\"role\":\"${r}\",\"content\":\"${c}\"\}"
set first 0
}
append new_body "\]\}"
HTTP::payload replace 0 [HTTP::payload length] $new_body
HTTP::header replace "Content-Length" [string length $new_body]
}
HTTP::header insert "X-PID-Score" $pid_total_score
HTTP::header insert "X-PID-Sanitized" "1"
HTTP::header insert "X-PID-ReqID" $pid_request_id
log $static::pid_log "PID: SANITIZED request_id=$pid_request_id score=$pid_total_score"
}
"LOG_ONLY" {
HTTP::header insert "X-PID-Score" $pid_total_score
HTTP::header insert "X-PID-ReqID" $pid_request_id
log $static::pid_log "PID: LOG_ONLY request_id=$pid_request_id score=$pid_total_score (forwarding)"
}
}
} else {
# Clean request — pass through with informational headers
HTTP::header insert "X-PID-Score" $pid_total_score
HTTP::header insert "X-PID-ReqID" $pid_request_id
}
}
# ==============================================================================
# JSON_REQUEST_ERROR: Malformed JSON is itself suspicious
# ==============================================================================
when JSON_REQUEST_ERROR priority 100 {
if { !$pid_inspect } { return }
log $static::pid_log "PID: malformed JSON client=$pid_client_ip uri=$pid_uri"
if { $static::pid_policy eq "BLOCK" } {
HTTP::respond 400 \
content "{\"error\":{\"type\":\"invalid_request\",\"code\":\"malformed_json\",\"message\":\"Request body could not be parsed.\"}}" \
"Content-Type" "application/json"
}
}
# ==============================================================================
# HTTP_RESPONSE: Propagate PID metadata into response headers
# ==============================================================================
when HTTP_RESPONSE priority 100 {
if { !$pid_inspect } { return }
if { [info exists pid_request_id] && $pid_request_id ne "" } {
HTTP::header insert "X-PID-ReqID" $pid_request_id
}
if { [info exists pid_total_score] && $pid_total_score > 0 } {
HTTP::header insert "X-PID-Score" $pid_total_score
}
}
Help guide the future of your DevCentral Community!
What tools do you use to collaborate? (1min - anonymous)