2026-08-27 · security · federated catalog

Your product description is now part of an agent’s reasoning

On a federated catalog, a free-text field from one merchant becomes context for every agent that searches. Read-only does not make that safe. Provenance does.

TL;DR — There is a class of attack on AI agents that requires no breach at all: put instructions in a field the agent will read, and wait. Commerce has a very large, very public, merchant-writable field that goes straight into an agent’s context — the product description. A federated index amplifies it, because one merchant’s text reaches every agent that searches the network. Our answer is not filtering. It is grading the source of every value and exposing that grade in the data.

The attack needs no exploit

The security literature calls this indirect prompt injection, and its sharpest form is tool poisoning: instructions hidden in the metadata an agent trusts by default — a tool’s description, a parameter schema, the body of a result. Microsoft’s own summary of the state of MCP security in 2026 puts it without hedging: an agent treats everything in its context as trustworthy, and anyone who can plant instructions in any of it can steer the agent.

This is not theoretical and it is not rare. MCPTox, a benchmark of tool poisoning against real-world MCP servers, reported attack success rates above 60% across major agents with a peak of 72% — and, counterintuitively, more capable models often did worse, because better instruction-following makes a model more compliant with malicious metadata. In May 2026, researchers at OX Security disclosed a systemic weakness across MCP implementations in Python, TypeScript, Java and Rust, with an estimated 200,000 vulnerable instances behind a supply chain measured in the hundreds of millions of downloads.

The most instructive demonstration is the smallest. In April 2026 a team including researchers from Johns Hopkins hijacked Claude Code, Gemini CLI and GitHub Copilot by putting malicious instructions in pull request titles. The agents read the PR data as ordinary task context, followed the injected instructions, and exfiltrated CI secrets into PR comments. Nothing was hacked. A text field was filled in.

A product title is a text field. So is a short description, an attribute value, a category name.

Why a federated index raises the stakes

A single store exposing its own catalog to agents has a contained problem: whatever is in its fields reaches agents that were already talking to that store.

A federated index is different by construction. It aggregates catalogs from independent merchants and serves them to agents that have never heard of any individual store. That is the entire point of it, and it is also the amplification: one careless or compromised merchant field is no longer one merchant’s problem, it is present in the result set of every agent that searches the network.

Our own index currently serves 4,591 canonicalised product rows across 10 merchant domains. Those numbers are small enough to be honest about — this is not Amazon — but the structure is the structure regardless of scale. Every one of those rows contains free text written by someone we do not control, and every one of them can end up in an agent’s context window.

Read-only helps. It does not solve this.

Our public surface is read-only and keyless. There is no write path, no mutation, no checkout, no payment. It is tempting to conclude that injection is therefore someone else’s problem. That conclusion is wrong, and it is worth being precise about why.

Read-only genuinely reduces the blast radius. The catastrophic outcomes of tool poisoning — the agent calls a destructive tool, exfiltrates a secret, moves money — need the agent to have a dangerous capability available. An agent talking to a catalog that can only answer questions has a smaller menu of harm.

But injection is an attack on the agent’s reasoning, not on the server’s write path. The realistic damage here needs no write at all:

The last one is why a read-only server does not get to opt out. We are one hop in a chain, and the poison travels.

The defence is provenance, not filtering

The instinct is to filter: scan descriptions for suspicious phrasing, strip anything that looks like an instruction, maintain a blocklist. That is the wrong shape of answer, for the same reason we do not maintain word lists anywhere else in this system. A blocklist is a patch per case. It is defeated by paraphrase, it degrades honest merchant copy, and it fails silently in a way nobody notices until it matters.

The answer that generalises is to stop treating all fields as equally authoritative, and to say so in the data.

We arrived at this from a different direction — a data-quality problem, not a security one — which is part of why we trust it. Before we tracked where a brand came from, our index held 793 brand values, and 12 of them were false. Not corrupted: inferred. The string dell had been attached to a BROSWAY necklace, extracted out of the Italian phrase “Albero Dell’…”. The brand apple had been attached to apple cider vinegar supplements. Nobody attacked us. Ordinary merchant text, read as if it were a declaration, produced confident nonsense.

The fix was to grade the source rather than the value. Three steps, one distinction exposed:

Declared always beats extracted. There is no blending. Counted across the whole served surface today — all 4,591 rows, not a sample — 3,046 rows carry a merchant-declared brand (66.3%) and 551 carry one extracted from content (12.0%). The remaining 994 rows (21.7%) have no brand at all, published as an absence rather than filled in with a guess. Brand and provenance now stand or fall together on every row: a brand_source never appears without the value it describes.

That is a security control, even though we built it as a data-quality control. An injected instruction lives in free text. Free text is exactly the tier we mark as lower authority. An agent receiving our results can tell which values a human configured and which a machine read out of prose, and can weight them accordingly — or refuse to act on the second kind.

The same principle runs through the rest of the surface. Every offer declares an authority_mode saying whether the value is federated-index truth or merchant-live truth, and carries a provenance.fetched_at. Every result from the federated MCP search tool declares where the search terms actually matched: a match_origin of name, description or category, alongside terms_matched and terms_in_name. Call global_search for ad alta temperatura and the mattresses come back with match_origin: description and terms_in_name: 0 — the agent is told, in the payload, that this hit came from a paragraph of marketing copy and not from a title. The REST catalog endpoint reports the same three fields for the same query. It also carries a second, narrower signal — a match object with matched_on, name_similarity and evidence_class — which describes the nature of a name-level hit and exists to expose false friends, where a query looks like a product name without being one. The two answer different questions and both are served: where the query landed, and how solid the name evidence is. None of that filters anything. It hands the agent the evidence and lets it decide, which is what a catalog owes an agent that has to be accountable for its own choices. It is the same argument we made about authority, freshness and consent, arriving from the security side.

What we have not solved

Three things, stated plainly, because a security post that ends in reassurance is not a security post.

We mark the tier, we do not sanitise the content. A determined injection in a merchant-declared field — one where the merchant themselves is the attacker, or their store is compromised — arrives carrying our highest authority marker. Declared beats extracted, but declared does not mean true. We have no answer to a hostile merchant beyond removing them from the network, which is a governance control operating at human speed.

Our tool descriptions are ours; our data is not. The classic tool-poisoning vector — malicious text in the tool metadata itself — we control and can audit. The catalog rows we serve come from ten independent WordPress installations whose security posture is not ours to guarantee.

The grading was right and the serving was wrong, until this week. While checking the figures for this article we found that 1,290 of 4,591 served rows carried a brand_source for a brand that was not in the payload — 1,217 of them saying merchant_declared. The stored data was correct; the list endpoint was assembling its response from two sources, taking the provenance from the canonical record and the brand itself from a projection where it was often absent. A human reading a card would never notice. An agent weighting by authority would read “the merchant declared this” over an empty field, which is precisely the failure this design exists to prevent. It is fixed: brand and provenance are now reconciled at a single point before the response is serialised, and the count of rows carrying one without the other is zero. We mention it because the defect was invisible from the inside and surfaced only when an outside reader counted brand presence where we had counted labels and got a different answer.

We do not know whether any of this has been tried on us. Our telemetry records what was searched, what was returned and by whom. It does not detect injection attempts in the content we ingest, because we are not looking for them. Claiming a clean record would be claiming a measurement we have not made.

What a merchant should do this week

Nothing exotic. The controls that matter here are the ones that were already good practice for being readable by agents at all.

And if you run any kind of aggregation — a feed, a marketplace, a federated index — the question to ask of your own architecture is not “can an attacker write to my database”. It is “whose text am I passing into someone’s reasoning, and does the receiver know which is which”.

Method note

Corpus figures count how many rows actually carry a brand value, not how many carry a provenance label — the two diverge, see above. They are a complete count of the served surface — every row returned by the public catalog endpoint of the KaliCart Global federated server, paginated end to end, not a sample and not the internal ingestion table, which also holds rows for merchants not currently published. They will drift as merchants join, leave and are re-ingested. The 793-brand figure and the twelve false attributions are from the pre-provenance state of the same index, recorded before the change.

Sources