K.B.code →
← back to lab

AI security

Hardening a pipeline against prompt injection

Content you did not write can hijack your model. Here is the layered sanitization I run, and exactly what it does not catch.

the problem

Any pipeline that reads the open web reads content nobody on the team wrote. It can carry a fake closing tag followed by 'ignore previous instructions'.

A single tag stripper feels safe and is not. Zero-width characters and unicode variants hide inside it, so the defense has to be layered and honest about its limits.

architecture

  raw external content
        |
   strip zero-width + control chars   <- NFKC alone misses these
        |
   normalize unicode (NFKC)
        |
   drop script/style, then strip tags
        |
   best-effort phrase net (not a boundary)
        |
        v
   inert text -> caller wraps it as DATA

The robust fix is structural, not clever. Sanitize to inert text, then have the caller wrap it in a delimiter the content cannot contain and tell the model the block is data. Scrubbing phrases is a backstop, never the boundary.

Order matters, and zero-width chars come off first.

import re, unicodedata

ZERO_WIDTH = re.compile(r"[\u200b\u200c\u200d\ufeff]")

def sanitize(text: str) -> str:
    text = ZERO_WIDTH.sub("", text)                 # hidden chars first
    text = unicodedata.normalize("NFKC", text)      # fold compatibility forms
    text = re.sub(r"<(script|style)\b[^>]*>.*?</\1>", " ", text, flags=re.I | re.S)
    text = re.sub(r"<[^>]*>", " ", text)            # then the generic strip
    return text.strip()

what is still broken

  • It reduces a real attack surface; it does not guarantee an injection-proof input.
  • The phrase net is English-only and best-effort. The actual boundary is the structural wrap, not the regex.