Signals Corps scanned HTTP response headers from 217 million Common Crawl page responses and counted 7,959 sites sending headers aimed at AI agents. Most were harmless. Pointers to llms.txt files, pointers to MCP servers. Polite signage for visiting bots.
Thirty sites went further and sent actual prompt injection attempts. Twenty-four of those used an X-AI header. Most were pranks. Some asked for real actions, like shell or database access.
Then they tested it. Claude models ignored the injected headers by default. But when the user prompt granted the site authority, the models obeyed the header in 7 of 9 runs. (Source: Signals Corps.)
So a header injection failed against a default model and mostly worked once the framing around it shifted. That second result is the one to worry about.
How the attack works
An HTTP response has a body and headers. Developers building agents with a fetch tool tend to think about the body: HTML, JSON, markdown. But plenty of agent stacks hand the model the whole response, status line and headers included, or surface selected headers as "metadata." Anything the model sees is text, and text from a remote server is attacker-controlled.
The mechanics are simple. A site (or anyone who controls the site's response headers, including a compromised CDN or reverse proxy) adds something like:
X-AI: <instruction-shaped text addressed to the agent>
If the agent's fetch tool passes that header into the context window, the model now has instruction-shaped text from an untrusted origin sitting next to the user's actual request. Whether the model acts on it depends on how the surrounding prompt frames that origin.
That's what the Signals Corps result shows. Default behavior: ignored. Prompt that grants the site authority: obeyed in 7 of 9 runs. An agent told "follow the site's guidance" is one X-AI header away from doing what the site says, including the requests for shell or database access that some of those 30 sites made.
The headers also don't look like a payload. There's no HTML to sanitize, no script tag, no rendered page. It's a short string in a field most tooling treats as plumbing.
The detection gap
Why does this slip through?
- Body-only sanitizers. If your pipeline scrubs fetched page content but passes headers through untouched, the header is a side door.
- Browser-era thinking. Headers were never an injection surface when the consumer was a browser. The consumer is now a language model that reads everything as prose.
- Relying on model defaults. The test shows Claude ignoring these headers by default. That's a property of one model under one prompt framing, not a control you own. It flipped with a single change to the user prompt.
-
Legitimate headers muddy the water. The vast majority of the 7,959 agent-targeted sites sent benign pointers. A blanket "block anything AI-addressed" rule would break real
llms.txtand MCP discovery. You need to score the content, not the header name.
Where Sentinel fits
The fix is to treat header text the same as any other untrusted content: run it through a scrub step before it enters the agent's context. Sentinel is the layer that does that scrub. I'm only claiming what the pipeline does with the text you hand it. I haven't tested Sentinel against the specific headers from the Signals Corps scan, so read this as how the layers apply to this class of input, not a verdict on their dataset.
For instruction-override text, the relevant layers are:
Fast-path regex (Layer 3). Our library of regex patterns covers authority hijacks like "ignore previous instructions" and "your new system prompt is," along with persona shifts and tool/function abuse patterns. An X-AI header that tries to override the agent's instructions is the textbook shape this layer is built for, and it runs at near-zero latency.
Deep-path vector similarity (Layer 4). Headers that skip the obvious phrasings get a semantic embedding compared against our library of attack signature embeddings. In standard mode, similarity above 0.55 neutralizes, above 0.40 flags, and above 0.82 blocks. Strict mode tightens neutralize and flag to 0.40 and 0.25.
Encoding and normalization (Layers 1 and 2). If a header hides its instruction in Base64, hex, or ROT13, Sentinel decodes and re-scans the result. Invisible characters and homoglyph tricks get normalized for scanning.
Where the benign headers go. An llms.txt pointer or MCP server URL isn't instruction-override text, so it shouldn't trip the pattern layers. Sentinel scores what the text says, which is how the harmless majority of that dataset passes through while the 30 injection attempts don't get a free ride.
One more behavior worth knowing: the descriptive-context exemption. If a header or page is quoting an injection ("attackers use strings like..."), the fast path defers to scored evaluation instead of hard-blocking. A real instruction with no descriptive framing still blocks immediately.
Example: scrubbing headers before the agent sees them
Illustrative. The header text below is made up to show the call shape; it is not taken from the Signals Corps dataset.
import httpx
SENTINEL = "https://api.sentinelaifirewall.com/v1/scrub"
KEY = {"X-Sentinel-Key": "sk_live_..."}
def fetch_for_agent(url: str) -> str:
resp = httpx.get(url, timeout=10)
# Serialize headers the way your agent would see them
header_text = "\n".join(f"{k}: {v}" for k, v in resp.headers.items())
# Scrub headers AND body as untrusted input
scrubbed = httpx.post(
SENTINEL,
json={"content": header_text + "\n\n" + resp.text, "tier": "strict"},
headers=KEY,
).json()
action = scrubbed["security"]["action_taken"]
if action == "blocked":
return "[fetch withheld: injection detected in response]"
return scrubbed["safe_payload"]
An illustrative response when an X-AI header carries instruction-override text:
{
"request_id": "a1b2c3...",
"security": {
"action_taken": "blocked",
"threat_score": 0.91
},
"safe_payload": "[SENTINEL BLOCKED]: Article withheld — fast-path prompt injection detected. Matched: \"...\"."
}
Branch on action_taken. On blocked, the agent never sees the header text. On neutralized, safe_payload carries the rewritten content with the offending span removed. On flagged, content passes through untouched and you decide whether to log, alert, or reject.
If you run an agent on the Anthropic SDK, the agentic proxy does this for tool results automatically. Point base_url at https://api.sentinelaifirewall.com/v1 and tool_result content gets scanned before it returns to the model. You'll need your own Anthropic key configured in Dashboard → Settings. One caveat: the proxy scans tool results, so a header only gets covered if your fetch tool returns it inside one. If you consume headers some other way, use the direct /v1/scrub call above.
What to do today
Find out whether your fetch or browse tool passes response headers into the model's context. Check the actual string the model receives, not what you assume it receives. If headers are in there, either strip everything except an allowlist you need (content-type, maybe content-length), or scrub the whole response as untrusted input before the model touches it.
And don't read "Claude ignored it by default" as a defense. The same test flipped to 7 of 9 obeyed once the user prompt told the model to trust the site. Prompts like that are routine in agent setups ("follow the site's instructions," "use the site's guidance"), so the safe default does not hold up under the framing you will actually ship.
Sentinel's Starter tier is free (100 requests/month, no credit card) if you want to run your own fetch output through /v1/scrub and see what comes back.
Try it: sentinelaifirewall.com
Sources
AI-assisted draft or imaging, human-curated, reviewed and edited.
Top comments (0)