Input security¶
Both protections run on the request before it is cached or sent to a provider.
PII redaction¶
Detection favours precision: dates, timestamps, version numbers, ISBNs and ordinary number
lists are left alone. Phone numbers in national formats other than North American ones (for
example 030 12345678) are only detected when written with a + country code.
Detected values are replaced with numbered placeholders. The same value always gets the same placeholder within a request, so the model can still reason about it:
"Refund jane@acme.com, card 4111 1111 1111 1111. Confirm to jane@acme.com."
→ "Refund [EMAIL_1], card [CREDIT_CARD_1]. Confirm to [EMAIL_1]."
| Entity | Detection |
|---|---|
email |
RFC-style address pattern |
phone |
International numbers written with + (8–15 digits) and North American (415) 555-0100 / 415-555-0100 / 415.555.0100 formats |
ssn |
US SSN format, excluding invalid ranges |
credit_card |
13–19 digits, Luhn-validated, with a real issuer prefix and card-style grouping |
ip_address |
IPv4 with octet validation (not after version, v, build...) |
iban |
IBAN format, mod-97 checksum-validated |
person |
spaCy PERSON entities (optional) |
from callm import PIIConfig
PIIConfig(
entities=("email", "phone", "ssn", "credit_card", "ip_address", "iban"),
action="mask", # or "block": raise PIIDetectedError, send nothing
ner=False, # True: also detect names (pip install "callm-toolkit[security]"
ner_model="en_core_web_sm", # and python -m spacy download en_core_web_sm)
roles=("system", "user", "assistant", "tool"),
)
Redaction applies to every text segment of the selected roles — plain strings, OpenAI and
Anthropic content blocks, Gemini parts and system instructions — and to text inside structured
fields: tool call arguments, Anthropic tool_use inputs and tool_result content, text
documents, and Gemini function calls and responses. Binary payloads (images, files) and thinking
blocks are never modified. Telemetry records how many
values of each entity were masked.
Standalone helpers:
from callm import redact_pii
from callm.security import PIIRedactor
redact_pii("call +1 415 555 0100") # 'call [PHONE_1]'
PIIRedactor(["email"]).find("a@b.io") # [PIIFinding(entity='email', start=0, end=6)]
Prompt injection detection¶
@callm(detect_injection=True) # flag
@callm(detect_injection=InjectionConfig(action="block")) # refuse
The default detector is a fast, dependency-free scorer with weighted signals for:
- instruction overrides ("ignore all previous instructions", including German, Spanish and French variants)
- system-prompt extraction ("repeat your system prompt verbatim")
- role hijacking and jailbreak personas ("you are now DAN", "developer mode enabled")
- requests to bypass safety filters or not follow rules
- fake chat-template tokens and role headers (
<|im_start|>system,[INST],### system:) - data exfiltration instructions and markdown image beacons
- hidden unicode (zero-width and bidi control characters) and large encoded payloads
Signals combine as independent evidence: score = 1 − Π(1 − weight). A score at or above
threshold (0.5) is flagged: callm logs a warning and records injection_flagged and
injection_score in telemetry — or raises PromptInjectionError with action="block".
By default only user and tool messages are scanned: tool results are the most common path
for indirect injection (a fetched web page telling the model what to do), while system prompts
are written by you.
Adding an ML classifier¶
InjectionConfig(
classifier="protectai/deberta-v3-base-prompt-injection-v2", # any HF text-classification model
classifier_label="INJECTION",
action="block",
)
The classifier runs locally with transformers (install it separately); the final score is the
higher of the heuristic and classifier scores.
Defence in depth
No detector catches every attack. Keep tools least-privileged, require confirmation for irreversible actions, and treat model output as untrusted input.