Skip to main content

New: Malicious MCP server caught in the wild!

research findings GenAI research findings

Hallucination or Cross-Tenant: When an LLM Answers With Another User’s Context

By:
Gadi Evron Gadi Evron
7 min read

Knostic Research recently reported a cross-tenant data exposure vulnerability in Cloudflare Workers AI affecting @cf/zai-org/glm-5.3.

Inference vulnerabilities are a class of their own, and while we will briefly walk through the vulnerability, Cloudflare’s blog post goes much deeper, including into the upstream vulnerability in SGLang, so if you’re interested in the technical details, you should read it first.

Our goal in writing this post is to explain how we used external blind analysis techniques to confirm this was a real issue, not a hallucination or a training data artifact.

Researcher: Gadi Evron (our CEO, who ran into the issue while working on an unrelated matter).

You can read their blog on the issue, released today, here: https://blog.cloudflare.com/kv-cache-exposure-remediation/

Kudos well-deserved

Gadi specifically wanted to share how working with the Cloudflare team. through the HackerOne platform, was a pleasure. The respect Cloudflare showed researchers, even beyond what’s reasonable, is an example of how a bug bounty program should run.

Special thanks go to Avishek Sarkar, Kevin Flansburg, and Michelle Chen.

What this blog is about

We focus on how we managed to determine what we saw wasn’t a hallucination, or an artifact of training data.

The vulnerability allowed an authenticated Workers AI customer to submit a sufficiently large request and receive content from another inference session instead of a response to their own prompt

LLMs can hallucinate, reproduce memorized material, lose context, or behave unexpectedly at large context sizes. The challenge was establishing that the returned material actually came from another workload.

Because we could not inspect Cloudflare’s serving infrastructure, we treated this as a black-box falsification problem: identify alternative explanations and try to eliminate them.

The observed leaks

We directly observed leaks in different categories, including:

  • Personal names
  • Client and company names
  • Invoice numbers
  • EUR monetary amounts
  • Payment dates
  • Source-code information
  • Filesystem and working-directory paths
  • Agent task records

One response used an absolute Linux filesystem path even though every request in the investigation was issued from macOS. Two others contained distinctive project identifiers that produced no results in the public code search, and one project was confirmed to have been created after the model training cut-off.

The failed boundary was tenant isolation between Workers AI customers. The attacker model required an ordinary paid Workers AI account, the attacker’s own API token, network access, and no victim credentials, privileged position, user interaction, or unusual configuration.

The first anomaly

The original anomaly appeared during normal work through OpenRouter, where we observed paths for a machine other than ours, Chinese language tutorials, and some other suspicious output.

Triage on September 7, 2026 identified Cloudflare as the relevant provider, at which point we switched our investigation to our direct Cloudflare account.

The affected endpoint was:

POST https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run/@cf/zai-org/glm-5.3

At the time of writing, Cloudflare documents @cf/zai-org/glm-5.3 as requiring paid access with a 1,310,720-token context window. While a request containing 50,851 prompt tokens returned HTTP 200 and correctly echoed a unique marker, an 812,377-token request returned:

* Correction to the above, kindly provided by Cloudflare’s team: “the statement about `@cf/zai-org/glm-5.3` having a `1,310,720` token context window should be adjusted. Engineering confirmed that while we currently report that value, it is a bug. The actual enforced context window is `1,048,576` tokens.”

HTTP 200
success: true
finish_reason: "stop"
cached_tokens: 0

But our marker was absent from both content and reasoning_content, and the output was unrelated to our prompt.

The request was nevertheless accounted for and billed in full at 103,604.64 neurons. That was anomalous, but by itself it was not evidence of a cross-tenant leak.

Did the model process our prompt?

Each request contained a unique marker at both ends:

ECHO-<UUID>

<deterministic filler>

End of input. Reply with exactly: ECHO-<UUID>

Below the failure boundary, the model returned the marker. Above it, the marker disappeared. Latency changed at approximately the same point:

Prompt tokensMarkerLatencyResult
50,851Present7.7 sHealthy
200,079Present16.2 sHealthy
400,113Present36.9 sHealthy
600,151Present60.1 sHealthy
662,162Present68.9 sHealthy
700,168Absent7.1 sAffected
750,179Absent8.7 sAffected
812,377Absent5.0 sAffected

Healthy-request latency increased with prompt size through 662,162 tokens. At roughly 700,000 tokens, it abruptly collapsed. A 700,168-token request completed in 7.1 seconds, compared with 68.9 seconds for the smaller 662,162-token request. At 812,377 tokens it took only 5.0 seconds.

This is consistent with affected requests not following the same prompt-processing path as healthy ones. Our working hypothesis was that the request was admitted and accounted for, but the submitted prompt was not successfully becoming the state used for generation.

That is a serving-behavior hypothesis, not a root-cause finding. The external evidence cannot tell us whether the underlying problem involved KV-cache handling, failed prefill, stale serving state, batching, worker reuse, context initialization, or something else.

Was it simply generating without context?

Requests with an empty user message returned an ordinary response noting that the message was empty. Requests using the raw prompt field with whitespace returned empty output. Neither resembled the unrelated material returned by oversized requests.

The affected responses therefore appeared conditioned on substantive state that was neither our submitted prompt nor the empty-context behavior we tested.

Was the output derived from our input?

We deliberately made the input simple and measurable. One affected request contained:

  • 3,027,810 characters
  • 33 distinct filler words
  • 812,377 prompt tokens

The filler was a repetitive NATO-phonetic-style vocabulary (Alfa Bravo Charlie Delta Echo Foxtrot Golf Hotel India Juliett Kilo Lima Mike November Oscar Papa Quebec Romeo Sierra Tango Uniform Victor Whiskey Xray Yankee Zulu), plus the unique marker.

The returned shell tool call contained identifiers occurring zero times in our input.

Another affected draw returned 528 completion tokens and 1,704 output characters. Against that response we measured:

  • 0 occurrences of our marker
  • 0 occurrences of our filler words
  • 0 hits on the English function-word set used in the test
  • 47 hits on a non-English function-word set
  • 4 currency-amount patterns
  • 4 invoice-number-shaped tokens
  • 2 calendar dates
  • 2 capitalized person-name bigrams

Our submitted prompt was entirely in English.

This established that the response was not a recognizable transformation or continuation of our input. It still did not establish where the output came from.

Could it be memorized training data?

Specific-looking source code or shell commands are not enough to prove a live data leak. Models can reproduce public training material.

One affected response gave us a stronger test.

At approximately 19:18 UTC, an 812,377-token request returned a shell tool call rather than our marker. It:

  • Named an absolute working directory
  • Referenced a YAML file beneath that directory
  • Performed a line-anchored search for three specific field names

None of those identifiers occurred in our prompt.

At the time of the report, we verified that:

  • A public repository contained the exact relative path
  • The file contained the same three field names at line starts
  • The repository’s own files described its agent stack as GLM-5.3 through Cloudflare
  • The repository had been created 23 days and 19 hours after the model’s recorded created_at in Cloudflare’s catalog
  • It contained a signature-verified commit less than four minutes after our affected inference

The returned material correlated with a project undergoing active development within minutes of our request.

Repeatability and boundary

The behavior repeated across multiple oversized requests on September 8:

UTCPrompt tokens
19:18:00812,377
19:18:23812,377
19:40:41700,168
19:40:49750,179
19:49:54700,170
19:50:04700,166
19:50:21700,170

The returned material varied between requests and included different technology stacks, operating-system paths, project identifiers, and task context.

The measured boundary was:

Prompt tokensBehavior
662,162Healthy
~700,000Affected behavior begins
~745,000Reliable reproduction margin
1,048,576Upper end of affected range
>1,048,576Clean HTTP 400

The lowest affected draw we measured was 700,166 tokens; 662,162 still behaved correctly.

Against the advertised 1,310,720-token context window, the observed transition occurred between approximately 50.5% and 53.4% of the advertised window.

The backend rejected requests above 1,048,576 tokens with HTTP 400, despite the 1,310,720-token advertised window, with no discernible security impact.

Sibling-model controls

We tested related Workers AI models with the same account and methodology.

@cf/zai-org/glm-5.3-flash, which advertises the same 1,310,720-token context window, answered correctly at 1,000,225 tokens and returned HTTP 400 at 1,200,259.

@cf/zai-org/glm-5.2 rejected an oversized request with HTTP 413.

@cf/zai-org/glm-4.7-flash answered correctly at 92% of its advertised context window.

These sibling deployments produced two isolation-safe outcomes: process the submitted context correctly, or reject the request rather than generate from unrelated state.

Only the tested GLM-5.3 deployment accepted the affected requests, accounted for their submitted tokens, and generated output unrelated to the submitted prompt.

Reproduction

The core reproduction required an ordinary authenticated Workers AI request:

ECHO-<UUID>

<large deterministic filler>

End of input. Reply with exactly: ECHO-<UUID>

with:

{
  "max_tokens": 2000,
  "temperature": 0
}

We measured usage.prompt_tokens and adjusted the filler until it crossed the affected range. Approximately 745,000 tokens provided a reliable reproduction margin, with smaller controls inserted between affected requests.

The minimal reproduction supplied to Cloudflare was:

ACCT=<my-account-id>
TOKEN=<my-api-token>
MARK="ECHO-$(uuidgen)"

python3 -c '
import json,sys
f=("` bravo charlie delta echo foxtrot golf hotel india juliet "*45000)
m=sys.argv[1]
print(json.dumps({
  "messages":[{
    "role":"user",
    "content":m+"\n\n"+f+"\n\nEnd of input. Reply with exactly: "+m
  }],
  "max_tokens":2000,
  "temperature":0
}))
' "$MARK" > body.json

curl -s \
  "https://api.cloudflare.com/client/v4/accounts/$ACCT/ai/run/@cf/zai-org/glm-5.3" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  --data @body.json

That filler produced approximately 761,000 prompt tokens in our testing. Exact tokenization may vary, so usage.prompt_tokens, not character or word count, is the relevant measurement.

The broader lesson

Underneath the AI-specific behavior is a conventional cloud-security problem: inference systems maintain state, and that state requires tenant isolation just like database rows, object storage, process memory, or application sessions.

Remediation and disclosure

Cloudflare Product Security statement:

“We have validated the issue and have deployed a fix.”

The coordinated-disclosure timeline:

  • September 7, 2026: The original OpenRouter anomaly was traced to Cloudflare as the relevant provider. Investigation moved to our own Workers AI account.
  • September 8: We reproduced and characterized the issue and submitted a report through HackerOne.
  • September 9: Cloudflare Product Security acknowledged the report and began its assessment.
  • September 24: Cloudflare reported that it had validated the issue and deployed a fix. The report was closed as Resolved.
  • October 9: Cloudflare requested our permission to report the issue upstream to SGLang.
  • October 26: Public disclosure by Cloudflare.