{"slug": "ansi-escape-injection-in-mcp-servers-hidden-from-humans-visible-to-ai", "title": "ANSI escape injection in MCP servers: Hidden from humans, visible to AI", "summary": "Security researchers have identified ANSI escape sequence injection (AESI) vulnerabilities in Model Context Protocol (MCP) servers that allow attackers to hide instructions from human reviewers while making them legible to AI agents. The attack exploits the gap between how terminals render ANSI control codes invisibly to humans and how language models process every raw byte, with two variants — direct-fetch AESI and stored AESI — targeting model-consumable fields such as tool results, resource reads, and prompt templates. Bright Security's DAST scanner can automatically detect these flaws by exercising running targets from the outside without source access.", "body_md": "# Detecting ANSI Escape Sequence Injection in MCP Servers with DAST\n\nANSI escape sequences were designed for terminals, not for language models. They are invisible control codes that tell a terminal to change colors, hide text, clear the screen, or move the cursor. A human reading a rendered terminal never sees the codes themselves — only their effect. A language model reading the same content sees every byte.\n\nThat gap is the whole attack. If an attacker can smuggle ANSI escape sequences into text that an AI agent consumes, they can hide instructions from the human reviewing the output while leaving those instructions perfectly legible to the model. This is the same class of output-neutralization failure seen in real products: Kubernetes `kubectl` ([CVE-2021-25743]([https://nvd.nist.gov/vuln/detail/CVE-2021-25743](https://nvd.nist.gov/vuln/detail/CVE-2021-25743))) accepted control sequences in Event strings that could spoof terminal output, and Git ([CVE-2024-52005]([https://github.com/git/git/security/advisories/GHSA-7jjc-gg6m-3329](https://github.com/git/git/security/advisories/GHSA-7jjc-gg6m-3329))) printed remote sideband messages without neutralizing terminal controls. In the Model Context Protocol (MCP) — where servers expose tools, resources, and prompts that stream text straight into an agent — every field the model reads is a potential injection surface.\n\nDAST is the natural way to catch this class of flaw. The Bright scanner exercises a running target from the outside, sends crafted inputs, and inspects real responses for evidence of a vulnerability — no source access required. That black-box, runtime posture is exactly what ANSI Escape Sequence Injection (AESI) demands, because whether a payload survives depends entirely on how the live server processes and relays it. This article covers two variants of the attack, **direct-fetch AESI** and **stored AESI**, what they put at risk, and, most importantly, how a DAST scanner detects them automatically.\n\n**What ANSI escape sequences are**\n\nANSI escape sequences date back to the 1970s, when hardware terminals from different vendors each spoke their own incompatible control dialect. The ANSI X3.64 standard unified them: a program could emit one agreed-upon set of codes to move the cursor, clear the screen, or set colors, and any conforming terminal would obey. They were invented to make text terminals controllable in a portable way — and that same control capability is what the attack abuses today.\n\nAn ANSI escape sequence starts with the escape byte (`ESC`, hex `0x1B`) followed by a control code the terminal treats as a command rather than printable text. Two categories matter here: **concealment** codes that make text invisible, and **screen and cursor** codes that clear the display, move the cursor, or erase a line to scrub or overwrite legitimate-looking output.\n\nThe key insight: rendering hides these bytes from people, but a model processes the raw text. What looks like an empty line or a tidy report to a human can carry a full set of instructions to the agent.\n\n**Attack 1: Direct-fetch AESI**\n\nMany MCP servers expose a tool that fetches a URL and returns its contents to the model — a “summarize this page” or “read this document” capability. The attack works like this:\n\n- The attacker hosts content laced with ANSI escape sequences and hidden instructions at a URL they control.\n- That URL is supplied to a fetch-style tool as an argument.\n- The server retrieves the content and relays it into a\n**model-consumable field**— a response location the agent actually ingests. - The model reads the concealed instructions and acts on them.\n\nThe concept to hold onto here is the **model-consumable field**. Not every part of an MCP response reaches the model. What matters are the specific fields an agent treats as content:\n\n- tool results — the text inside result.content[].text\n- resource reads — the text inside result.contents[].text\n- prompt templates — the text inside result.messages[].content.text\n\nA payload that surfaces anywhere else is not exploitable. This is a form of cross-prompt injection: the malicious instructions ride in on *data* the agent was asked to process, not on the user’s own prompt.\n\n**Attack 2: Stored AESI**\n\nStored AESI is the same payload with a more dangerous delivery mechanism. Instead of the server fetching a live URL, the attacker **writes** the payload into storage through one entrypoint — a note, a comment, a record — and it **detonates later** when a *different* entrypoint reads that data back into a model field.\n\nThree properties make the stored variant worse than direct-fetch:\n\n**Persistence.** The payload sits in storage and fires again on future sessions, potentially against other users.**Decoupling.** The write and the read are different entrypoints, and they can even use different protocols. A payload written through an ordinary HTTP endpoint can resurface when the agent reads the same record back through an MCP tool or resource.**Delayed exposure.** Nothing looks wrong at write time. The injection only becomes active when some later, unrelated read pulls the stored text into the model’s context.\n\nCrucially, you cannot infer these write-then-read paths from parameter names, tool descriptions, or protocol type. The only reliable way to know where written data resurfaces is to probe for it empirically — which is exactly what the automated detection does.\n\n**Security impact**\n\n**Unauthorized agent actions.** Concealed instructions can cause the model to invoke tools, disclose data, alter analysis, or produce attacker-controlled output within the agent’s existing privileges.\n\n**Human-in-the-loop bypass.** ANSI concealment can hide attacker instructions from the user supervising or approving the agent. The visible response appears benign, while the model still receives the raw payload and may follow it.\n\n**Log and audit trail manipulation.** Screen-clear, cursor-movement, and line-erasure sequences can spoof terminal or log-viewer output, obscuring activity during review and incident response.\n\n**Persistent injection risk.** In stored AESI, the payload remains in application data and can reappear through later MCP tools, resources, or prompts. A single write can affect future sessions, other users, and cross-protocol read paths.\n\nIn practice, one poisoned record in a shared knowledge base can influence every later agent workflow that reads it. The risk is not just a misleading summary; it is attacker-controlled data reaching an agent with access to tools, credentials, and downstream systems.\n\n**Automating detection with DAST**\n\nThe heart of this work is turning the attack into a repeatable, automated test. The detection is built on a few design principles, then applied through two pipelines — one for direct-fetch, one for stored.\n\n**Design principles**\n\n**Inspect raw bytes.** The ANSI escape bytes are the signal, so any rendering, sanitizing, or normalizing before inspection destroys it. Detection reads the raw HTTP/MCP response bytes.**Only model-consumable fields count**— the sink. The classifier walks the JSON paths a model actually ingests (tool content, resource contents, prompt message text) and ignores everything else.**Unique trigger markers.** Each payload carries its own improbable marker, so a match ties an effect back to exactly one injection — the mechanism black-box confirmation depends on.**Three-signal confirmation.** A field is vulnerable only when an ANSI escape byte, a fixed instruction phrase, and the payload’s trigger all survive together in the same model-consumable field. Requiring all three is what suppresses false positives.**Recurse into encoded values.** The classifier descends into stringified JSON so a payload wrapped inside a JSON-encoded field is still caught.\n\nDetection is therefore **binary** — the three-signal rule holds or it doesn’t — which is what makes the check low-noise enough to automate.\n\n**The payload corpus**\n\nInjections are driven from a small corpus of hosted payload files, each exercising a different ANSI technique (concealment, background-matched foreground, clear-screen, cursor-erase, and an HTML-wrapped variant) with its own trigger. Variety matters because a server that strips one technique may still relay another.\n\n**Direct-fetch detection pipeline**\n\nThe direct test targets MCP action-request entrypoints (tools/call, resources/read, prompts/get) that have at least one parameter. It does **not** try to guess which parameters are “URL-like” — it treats a broad set of text parameter types (string, URL, email, phone, file-URI) as injectable.\n\n**Inject.** For each candidate entrypoint and each payload in the corpus, substitute the payload’s hosted URL into an injectable text parameter and invoke the entrypoint once. The expectation is that the server fetches that URL and relays its contents.**Capture.** Record the full MCP response as raw bytes. Responses that are unsuccessful or that carry a JSON-RPC error are simply skipped — they produce no finding, and there is no separate “safe” classification to record.**Classify.** Walk the model-consumable fields and apply the three-signal rule. If an ANSI byte, the instruction text, and the trigger all survive in one such field, that field is vulnerable.**Report.** File one issue per vulnerable parameter — the direct test deliberately allows multiple issues per entrypoint so that each independently exploitable parameter is surfaced. Each finding records the payload URL that triggered it and the field path where the payload was detected.\n\n**Stored detection pipeline**\n\nThe hard problem in the stored case is not classification — it is discovering where written data resurfaces. The pipeline solves that empirically in two phases before it ever sends a real payload.\n\n**Phase 1 — Probe.** Assign one unique, inert marker to each injection entrypoint and write it embedded in benign carrier text. Using one marker per source is what makes a later match attributable to exactly one write. Injection candidates are:\n\n- MCP tools/call or prompts/get entrypoints with a text parameter inside the call’s arguments, or\n- HTTP POST/PUT/PATCH endpoints with a writable text parameter.\n\nIf a write is rejected at ingestion, that entrypoint is excluded — no reflection is possible from it.\n\n**Phase 2 — Map reflections.** Read back every reflection candidate and scan the raw response for any probe marker. For stored AESI, reflection candidates are **MCP action-request entrypoints only** — the payload has to resurface in an MCP model-consumable field to be exploitable, so a plain HTTP GET is not a qualifying read surface here. When a marker is found, the engine records the exact JSON field path where it surfaced. Reflections that don’t land in a model-consumable field path are discarded. The result is a map: each injection entrypoint points to the read surfaces (and field paths) where its data reappears.\n\nThis is also where the cross-protocol case shows up in practice: a marker written through an HTTP endpoint may be found when the same record is read back through an MCP tool or resource. The write side can be HTTP or MCP; the qualifying read side is MCP.\n\n**Phase 3 — Attack and confirm.** For each mapped path, write the real AESI payload through the injection entrypoint, then re-read the mapped MCP reflection surfaces and apply the classifier — but only at the **recorded field path** that the probe phase already proved is model-consumable. Anchoring confirmation to that exact path keeps the test precise and low-noise. Reporting short-circuits: once a stored injection is confirmed for a given injection entrypoint, the test files a single **stored AESI** issue for it and stops. The finding records the payload URL, the reflection entrypoint, and its MCP method and name.\n\nThe probe phase is what makes this tractable: it converts an unknown source-to-sink topology into a concrete, field-path-anchored map, so payload delivery targets exactly where data was already proven to surface. That source-to-sink modeling is what separates a real DAST finding from a naive reflection guess.\n\n**Defenses and takeaways**\n\nFor teams building or integrating MCP servers:\n\n**Sanitize control bytes** out of any fetched or stored content before it is placed into a model-consumable field. Stripping ANSI escape sequences at that boundary removes the payload’s concealment mechanism.**Validate ingestion.** URL allow-lists and input validation on write endpoints stop many payloads before they are ever fetched or stored.**Treat all external and stored text as untrusted** the moment it can reach the model — regardless of which protocol or entrypoint delivered it.**Scan continuously.** Automated, byte-level, field-aware detection belongs in CI and DAST against MCP surfaces, covering both the direct-fetch and stored paths.\n\nThe recurring lesson is that effective detection has to model the *data flow*, not just individual requests. A single request rarely tells you whether text reaches the model; mapping where data is written and where it resurfaces is what turns a guess into a confirmed, exploitable finding.\n\n**Conclusion**\n\nAESI shows that AI agents inherit every ambiguity of the formats they consume: a 1970s cursor code becomes an injection primitive the moment a model, not a terminal, reads the bytes. As MCP feeds agents more untrusted text, the next attacks will come less from novel exploits than from old representation quirks reaching a consumer never meant to interpret them.\n\nCatching them belongs in DAST, because survival depends on what the live server fetches, stores, strips, and resurfaces — none of it visible from the code. The concealment that makes AESI dangerous to a human reviewer is exactly what makes it tractable to automate: the model sees every byte, and so should the scanner.", "url": "https://wpnews.pro/news/ansi-escape-injection-in-mcp-servers-hidden-from-humans-visible-to-ai", "canonical_source": "https://brightsec.com/research/detecting-ansi-escape-sequence-injection-in-mcp-servers-with-dast/", "published_at": "2026-07-21 07:01:24+00:00", "updated_at": "2026-07-21 07:22:41.576203+00:00", "lang": "en", "topics": ["ai-safety", "ai-agents", "ai-infrastructure", "ai-tools"], "entities": ["Model Context Protocol", "Bright Security", "ANSI X3.64", "CVE-2021-25743", "CVE-2024-52005", "Kubernetes", "Git"], "alternates": {"html": "https://wpnews.pro/news/ansi-escape-injection-in-mcp-servers-hidden-from-humans-visible-to-ai", "markdown": "https://wpnews.pro/news/ansi-escape-injection-in-mcp-servers-hidden-from-humans-visible-to-ai.md", "text": "https://wpnews.pro/news/ansi-escape-injection-in-mcp-servers-hidden-from-humans-visible-to-ai.txt", "jsonld": "https://wpnews.pro/news/ansi-escape-injection-in-mcp-servers-hidden-from-humans-visible-to-ai.jsonld"}}