Security9 of 10
Fetching and third-party content
On this page
Two guards sit between the outside world and the agentic loop. Both are filters, and both are described here as filters rather than boundaries.
The URL guard
Anything that fetches a URL the model can influence goes through one function first. There are four callers:
web_extract— reading a pageremember_image— saving an image she was shown- the Live2D model download
- connecting a remote MCP server, whose URL comes off a public registry
Without a guard, "read http://169.254.169.254/…" — cloud instance metadata, and the credentials
behind it — or any internal endpoint would be reachable through Kotoba from the outside.
What it allows
Only http and https. Every other scheme is refused by name.
Then the hostname is resolved to all its addresses, and the fetch is refused if any of them is:
- loopback
- private (RFC 1918 and equivalents)
- link-local — which is what covers
169.254.0.0/16andfe80::/10 - reserved
- multicast
- unspecified
An IPv4-mapped IPv6 address is unwrapped and re-checked. An address that cannot be parsed is treated as unsafe. A hostname that cannot be resolved is refused rather than fetched: what cannot be vetted is not fetched.
Four names are blocked outright as a cheap first cut, before resolution: localhost,
metadata.google.internal, metadata, instance-data.
Redirects
Redirects are not followed automatically. web_extract, remember_image and the model download
each follow them manually so that every hop is checked again. A redirect to a blocked address
stops there.
What it does not cover
A true DNS rebind stays open. The name is resolved to vet it, and resolved again to connect; a resolver that answers differently between those two moments defeats the check. This is stated in the code and is not fixed by it.
One deliberate opt-out exists. KOTOBA_ALLOW_LOCAL_MCP=1 disables the guard for remote MCP
server URLs only, so you can connect a server you run yourself on localhost or the LAN. It affects
nothing else, and stdio servers never carry a URL. Leave it unset unless you need it, because the
URL of a discovered server is exactly the model-influenced input the guard exists for.
The prompt-injection screen on MCP tools
An MCP server supplies a tool's name, its description and every string in its JSON schema. All of it is fed to the model. A malicious or compromised server can put instructions there.
Before a tool is registered, its whole model-facing surface — name, description, and every string value inside the input schema, to a depth of eight, in dicts and lists alike — is scanned. A match means the tool is refused and logged; it is never offered to the model.
The surface is the whole document, not a list of interesting keys: the schema ships verbatim as the tool's parameters, so a payload nested in a property's description reaches the model exactly like the top-level one. Only the JSON Schema grammar keywords are skipped, because their values are grammar rather than prose.
The same scan filters candidates returned by a registry search, before you are even shown them.
What it looks for
Fourteen patterns, in English and Spanish: instruction-override phrasing; "disregard the system";
"you are now"; requests to reveal, exfiltrate, send or dump a key, token, secret, password or
credential; "always call this tool"; fake role tags in four shapes (<system>, chat-template markers,
a role word as a heading, a role word as a label); "do not tell the user"; overriding safety, security,
approval or permission; a reference to a well-known secret path; and a claim to be pre-approved.
A description longer than 4000 characters is refused on length alone: at that size it is not documentation.
The normalising pass matters more than the patterns
Patterns are matched against what the model effectively reads, never the raw bytes. The text is put through: invisible and formatting characters resolved (soft hyphen, zero-width space, joiners, word joiner, BOM, the direction marks and the bidi overrides), compatibility forms folded, combining marks stripped so the Spanish patterns match with or without accents, and whitespace collapsed.
Invisibles are tried both ways — deleted and replaced with a space — because there is no single right answer: deleting a zero-width space joins two words into one token that no word-separated pattern matches, while turning it into a space breaks a word split by a soft hyphen. A model reads both as the plain sentence, so both variants are scanned.
Measured against samples: an ordinary tool description is accepted; a plain-English override, a soft-hyphenated one, a Spanish one in natural adjective order, a fake role tag, a chat-template marker, a reference to an SSH key path, a "pre-approved for all actions" claim, and a 4100-character description are all refused.
The honest limit
This is a filter, not a boundary, and it cannot be complete. It runs once, over what a server advertises. It does not scan what a server returns at call time.
The real defence is the architecture, not the regular expressions: fetched and returned text is data, never instructions, and anything that touches your machine is gated by an approval card regardless of what any text asked for.
Untrusted text on screen
Text somebody else wrote is made safe to draw before it is drawn, in the terminal renderer and on Discord cards. Two families are handled differently:
- Cursor moves — control characters,
DEL, the C1 range, and the line/paragraph separators — become a space. A carriage return plus erase-line can paint a dangerous command, wipe the row and paint a harmless one over it, so an approval card would ask about something that was on screen for no frames at all. They become spaces rather than being deleted, so every readable byte still shows. - Reordering and hiding — the bidi overrides and isolates, the direction marks, zero-width space, soft hyphen, word joiner, BOM, interlinear annotation, and the invisible tag block — are removed. A visible space there would hand an attacker the word boundary.
Left alone on purpose, because the line is override versus script: right-to-left scripts themselves, zero-width joiner and non-joiner, variation selectors, and combining marks.
Files she writes, opened in a browser
A file she wrote and you open in a tab is untrusted content served from your own origin. Every
response whose media type could be a document leaves with a sandbox CSP, including .svg and
.xhtml. See The web gate.
