Security checks
Prompt injection and personal data, checked before a request leaves for a provider.
Turning it on
Every organisation starts in flag mode: requests are scanned and findings recorded, and nothing is ever refused. A check that has to be discovered and switched on is one almost nobody switches on, and flagging changes nothing about whether a request succeeds.
Change the default on the Security page in the dashboard, or set it per request with an x-vatan-security header. Three modes: off scans nothing, flag records what it finds, block refuses a request carrying a high-severity finding.
curl https://api.vatan.one/v1/chat/completions \ -H "Authorization: Bearer $VATAN_GATEWAY_API_KEY" \ -H "x-vatan-security: block" \ -H "Content-Type: application/json" \ -d '{"model":"openai/gpt-4o-mini","messages":[{"role":"user","content":"Hello"}]}'
An unrecognised header value falls back to your organisation's setting rather than to off. A typo should not take your traffic down, and it should not quietly disable a security control either.
Prompt injection
Six patterns, each recognising a shape that turns up in real attempts: instruction overrides (ignore all previous instructions), attempts to read back the system prompt, forged chat-template markers like <|im_start|>system, persona overrides, requests to bypass safety, and long encoded payloads. The first three are high severity and block; the rest flag.
System messages are never checked for injection. A system prompt is your own instruction to your own model, so ignore any previous instructions the user gives you is you writing your app, not somebody attacking it. Flagging that would make the check noise, and a noisy check gets switched off.
Personal data
Eight kinds: email addresses, card numbers, US social security numbers, UK National Insurance numbers, IBANs, phone numbers, API keys and IP addresses. Card numbers, IBANs and API keys are high severity and block; the rest flag.
Card numbers are checked against the Luhn checksum and IBANs against mod-97. A sixteen-digit run is far more often an order number, an id or a timestamp than a card, and a check that fires on all of those is one somebody turns off within the week.
Unlike injection, personal data is checked in system messages. A customer record pasted into a system prompt reaches the provider exactly like one pasted into a user message.
Findings are stored as kinds and counts only. The matched text is never recorded, because writing the card number into the audit trail to prove a card number was found would defeat the point of finding it.
What a blocked request looks like
A blocked request answers 400 security_check_failed, naming the kinds that were found. Nothing is sent to a provider and nothing is charged.
{ "error": { "message": "The request was stopped by a security check: instruction_override. Send x-vatan-security: flag to record findings without blocking.", "type": "security_check_failed", "code": "security_check_failed" } }
It is still recorded in your request log, at zero cost, with the findings attached. “We stopped forty requests this week” is most of the value of the feature, and it cannot be reported from rows that were never written.
What it does not do
Both checks are heuristic string matching. They recognise the shapes real attempts and real identifiers take, not every possible one. A determined attacker will phrase an override a way these patterns do not match, and an identifier format not on the list will pass unnoticed.
So treat this as a net that catches the common case and tells you it happened, not a guarantee. Most injection attempts are copied from somewhere, and most leaked personal data is somebody pasting a customer record into a prompt without thinking; both are exactly what this catches. Neither is the case you should design your threat model around.
It is said this plainly because a control people trust more than it deserves is worse than no control at all: it moves traffic through a gate everybody assumes is shut.