Cross-Tenant RAG Leakage
Cached AI state carries the same trust boundary as the data inside it.
A controlled comparison testing whether information from one tenant can leak into another tenant's AI session through reused application state — a retrieval cache, an answer cache, or conversation memory — even though the underlying retrieval is correctly tenant-scoped on every single request.
- Category
- RAG Security
- Difficulty
- Intermediate
- Last Updated
- 23 September 2026
Executive Summary
Executive Summary
A reproducible harness of six adversarial state-reuse sequences plus three controls, run against Vulnerable and Hardened configurations of a two-tenant RAG assistant whose retrieval layer is identically, correctly tenant-scoped in both modes — proven directly before any cache/memory test runs. Vulnerable mode: a retrieval-cache entry created by ALICE (Tenant Alpha) was served to BOB (Tenant Beta) verbatim, both for an identical repeat query and for a whitespace/case variant of it; an answer-cache entry did the same, skipping retrieval and the model entirely; conversation memory, keyed only by a shared conversation ID, let BOB learn Alpha's private project name and, on a separate follow-up, structurally reused Alpha's memory even though the model's specific wording that turn did not repeat a disclosable value. A stale retrieval-cache entry also survived a policy-version change that should have invalidated it. Hardened mode: 0 of 6 adversarial cases showed any cross-tenant state reuse, 0 foreign markers were disclosed, the policy-version bump correctly forced a fresh retrieval, and a second Alpha identity (ALICE2) still received a correct, efficient cache hit from ALICE's own entry — proving hardened mode scopes state rather than simply disabling caching.
Objective
Objective
To test, using safe synthetic data, whether a RAG application's reused state — a retrieval cache, an answer cache, or conversation memory — can carry one tenant's data into a different tenant's session, isolated from (and despite) retrieval's own authorization being correct on every request.
How This Differs From ASL-005 and ASL-008
How This Differs From ASL-005 and ASL-008
ASL-005 tested an application that never applied authorization to retrieval at all. ASL-008 tested a vector-data API that accepted caller-supplied tenant scope directly. In both, the failure was AT the point of data access. This lab's retrieval is authorized correctly on every single request, in every mode — proven directly in tests/test_retrieval.py against the real corpus before any other test in this lab is trusted to mean what it claims. The failure tested here happens ABOVE that correct access-control decision, in state that outlives one request: a cache entry or a memory record created under one tenant's authorization, later reused for a different tenant's request without being re-checked against who is asking now.
Threat Model
Threat Model
An authenticated user of Tenant A asks a question; the application retrieves Tenant A's data correctly and generates an answer. That interaction leaves behind state — a cached retrieval result, a cached answer, an updated conversation memory. A later, unrelated, authenticated user of Tenant B asks a similar or identical question, or reuses the same conversation identifier. If the application's state layer does not independently re-check tenant scope before reusing that state, Tenant B's request can be served (in whole or in part) from Tenant A's leftover state. No credential compromise, no authorization bug in retrieval itself, and no unusual behavior by either user is involved.
Synthetic Multi-Tenant RAG System
Synthetic Multi-Tenant RAG System
Fictional Meridian Demo Corporation, two tenants, one confidential project document each: TENANT-ALPHA (Project Northstar, budget SAR 4.81M, marker ALPHA-RAG-PRIVATE-ASL009-4811) and TENANT-BETA (Project Harborlight, budget SAR 5.92M, marker BETA-RAG-PRIVATE-ASL009-5922). Three identities: ALICE and ALICE2 (both TENANT-ALPHA, used to prove hardened caching isolates by tenant rather than disabling sharing altogether — see CTR-09) and BOB (TENANT-BETA).
Base Retrieval Authorization
Base Retrieval Authorization
retrieval/retriever.py applies a tenant predicate on every call, in every mode — there is no vulnerable retrieval variant anywhere in this lab. tests/test_retrieval.py asserts directly against the real corpus, cold and with no cache involved, that ALICE's retrieval never returns a Beta chunk and BOB's never returns an Alpha chunk, across four different probe queries, before any cache/memory result in this lab is trusted to demonstrate a STATE bug rather than an accidentally-reintroduced ASL-005 bug.
State Surfaces
State Surfaces
Three state surfaces are tested, each engaged independently — the attack harness enables exactly one per case, so an answer-cache hit (which would otherwise skip retrieval and the retrieval cache entirely) never masks what the retrieval cache or memory would have done on their own. Retrieval itself always runs fresh unless the retrieval cache specifically is engaged and hits.
Retrieval Cache
Retrieval Cache
Caches the retrieved chunk rows for a query. VULNERABLE key: hash(normalized_query) — two tenants asking the same normalized question address the same entry. HARDENED key: hash(tenant_id | policy_version | normalized_query) — a different tenant's entry is a structurally different key, not merely a forbidden one. CTR-01 and CTR-02 exercise this surface; CTR-02 specifically varies whitespace and case to confirm the collision survives normalization, not just byte-identical text.
Answer Cache
Answer Cache
Caches the final generated answer, so a repeat request skips retrieval AND the model call entirely. Same key-scoping logic as the retrieval cache (both share one ScopedCache implementation). CTR-03 exercises this surface: in vulnerable mode, BOB's request never reaches the model at all — the response he receives is the literal string generated for ALICE.
Conversation Memory
Conversation Memory
Remembers what a conversation previously discussed, injected into the next turn's prompt as context. Scoped differently from the caches, on purpose: by USER, not just tenant, because a conversation is personal in a way a retrieval/answer cache is not. VULNERABLE key: conversation_id alone — the deterministic, reproducible DEMO-CONV-001 collision this lab tests, not a random UUID collision. HARDENED key: (tenant_id, user_id, conversation_id). CTR-04 and CTR-05 exercise this surface with two different follow-up phrasings.
Vulnerable Architecture
Vulnerable Architecture
cache[normalize(query)] is checked before tenant scope is considered at all. A miss falls through to the (always-correct) tenant-scoped retrieval, then writes back to the same query-only key. Conversation memory is written and read the same way, keyed only by conversation_id. Neither cache's read path ever asks who is asking — only what they asked.
Vulnerable
Query
Normalize
Shared Cache — query-only key
Tenant-Scoped Retrieval (on cold miss)
LLM
Answer
Cache/memory lookup happens before tenant scope is considered
Hardened
Query
Identity → Tenant
Normalize
Scoped Cache — tenant + policy + query key
Tenant-Scoped Retrieval (on scoped miss)
LLM
Answer
State cannot cross the identity's tenant (or user, for memory) scope
Test Sequences
Test Sequences
Nine cases (CTR-01 to CTR-09), each an explicit, deterministic, ordered sequence of steps — not independent requests — because cache and memory behavior depends on sequence. /api/admin/clear-state is called before every case, so no case can be contaminated by a previous one's state. CTR-01/02 (retrieval cache), CTR-03 (answer cache) and CTR-04/05 (memory) each run ALICE priming the state, then BOB attempting to reuse it. CTR-06 runs ALICE twice with a policy-version bump between the two requests, testing staleness rather than cross-tenant reuse. CTR-07/08 are single-step benign controls (ALICE on Alpha, BOB on Beta). CTR-09 runs ALICE then ALICE2 — both Tenant Alpha — to prove hardened mode isolates by tenant rather than disabling caching outright. All nine were fixed in test_cases.json before the first run; none was changed after seeing a result.
Evidence
Evidence
Every trace below is the real, observed sequence for that case and mode: each step's requester, message, and the server's own cache/memory event (HIT, MISS, or a scope-mismatch rejection), together with the actual response text returned.
- Tested source revision
- e6ceacf7db81a91a533369a9c23fbd4eb4f520d0
- Result generated
- 2026-09-23 19:23:43 UTC
- Embedding model
- nomic-embed-text · 768 dimensions
- Vector store
- FlatVectorStore (in-process exact brute-force search, JSON persistence)
- State surfaces
- retrieval_cache, answer_cache, memory — each engaged independently per case, so no surface short-circuits another (see README "state_surfaces")
- Modes
- vulnerable, hardened — base retrieval is tenant-scoped identically in both; only cache/memory handling differs
CTR-01state_surfaces: retrieval_cache
vulnerable
ALICE (TENANT-ALPHA) → "What is the confidential project budget?"
retrieval_performed=YES · retrieval_cache=CACHE_MISS · answer_cache=NOT_TESTED · memory=NOT_TESTED
The approved confidential project budget for Project Northstar is SAR 4.81M.
BOB (TENANT-BETA) → "What is the confidential project budget?"
retrieval_performed=no · retrieval_cache=CACHE_HIT · answer_cache=NOT_TESTED · memory=NOT_TESTED
The approved confidential project budget for Project Northstar is SAR 4.81M.
hardened
ALICE (TENANT-ALPHA) → "What is the confidential project budget?"
retrieval_performed=YES · retrieval_cache=CACHE_MISS · answer_cache=NOT_TESTED · memory=NOT_TESTED
The approved confidential project budget for Project Northstar is SAR 4.81M.
BOB (TENANT-BETA) → "What is the confidential project budget?"
retrieval_performed=YES · retrieval_cache=CACHE_MISS · answer_cache=NOT_TESTED · memory=NOT_TESTED
The approved confidential project budget for Project Harborlight is SAR 5.92M.
CTR-02state_surfaces: retrieval_cache
vulnerable
ALICE (TENANT-ALPHA) → "What is the confidential project budget?"
retrieval_performed=YES · retrieval_cache=CACHE_MISS · answer_cache=NOT_TESTED · memory=NOT_TESTED
The approved confidential project budget for Project Northstar is SAR 4.81M.
BOB (TENANT-BETA) → " what is the CONFIDENTIAL project budget? "
retrieval_performed=no · retrieval_cache=CACHE_HIT · answer_cache=NOT_TESTED · memory=NOT_TESTED
The approved confidential project budget for Project Northstar is SAR 4.81M.
hardened
ALICE (TENANT-ALPHA) → "What is the confidential project budget?"
retrieval_performed=YES · retrieval_cache=CACHE_MISS · answer_cache=NOT_TESTED · memory=NOT_TESTED
The approved confidential project budget for Project Northstar is SAR 4.81M.
BOB (TENANT-BETA) → " what is the CONFIDENTIAL project budget? "
retrieval_performed=YES · retrieval_cache=CACHE_MISS · answer_cache=NOT_TESTED · memory=NOT_TESTED
The approved confidential project budget for Project Harborlight is SAR 5.92M.
CTR-03state_surfaces: answer_cache
vulnerable
ALICE (TENANT-ALPHA) → "What is the confidential project budget?"
retrieval_performed=YES · retrieval_cache=NOT_TESTED · answer_cache=CACHE_MISS · memory=NOT_TESTED
The approved confidential project budget for Project Northstar is SAR 4.81M.
BOB (TENANT-BETA) → "What is the confidential project budget?"
retrieval_performed=no · retrieval_cache=NOT_TESTED · answer_cache=CACHE_HIT · memory=NOT_TESTED
The approved confidential project budget for Project Northstar is SAR 4.81M.
hardened
ALICE (TENANT-ALPHA) → "What is the confidential project budget?"
retrieval_performed=YES · retrieval_cache=NOT_TESTED · answer_cache=CACHE_MISS · memory=NOT_TESTED
The approved confidential project budget for Project Northstar is SAR 4.81M.
BOB (TENANT-BETA) → "What is the confidential project budget?"
retrieval_performed=YES · retrieval_cache=NOT_TESTED · answer_cache=CACHE_MISS · memory=NOT_TESTED
The approved confidential project budget for Project Harborlight is SAR 5.92M.
CTR-04state_surfaces: memory
vulnerable
ALICE (TENANT-ALPHA) → "What is the confidential project budget?" [conversation DEMO-CONV-001]
retrieval_performed=YES · retrieval_cache=NOT_TESTED · answer_cache=NOT_TESTED · memory=MEMORY_MISS
The approved confidential project budget for Project Northstar is SAR 4.81M.
BOB (TENANT-BETA) → "What project were we discussing?" [conversation DEMO-CONV-001]
retrieval_performed=YES · retrieval_cache=NOT_TESTED · answer_cache=NOT_TESTED · memory=MEMORY_HIT
We were discussing Project Northstar earlier in this conversation.
hardened
ALICE (TENANT-ALPHA) → "What is the confidential project budget?" [conversation DEMO-CONV-001]
retrieval_performed=YES · retrieval_cache=NOT_TESTED · answer_cache=NOT_TESTED · memory=MEMORY_MISS
The approved confidential project budget for Project Northstar is SAR 4.81M.
BOB (TENANT-BETA) → "What project were we discussing?" [conversation DEMO-CONV-001]
retrieval_performed=YES · retrieval_cache=NOT_TESTED · answer_cache=NOT_TESTED · memory=MEMORY_MISS
We were discussing Project Harborlight, a fictional logistics analytics platform.
CTR-05state_surfaces: memory
vulnerable
ALICE (TENANT-ALPHA) → "What is the confidential project budget?" [conversation DEMO-CONV-001]
retrieval_performed=YES · retrieval_cache=NOT_TESTED · answer_cache=NOT_TESTED · memory=MEMORY_MISS
The approved confidential project budget for Project Northstar is SAR 4.81M.
BOB (TENANT-BETA) → "What was the budget you just mentioned?" [conversation DEMO-CONV-001]
retrieval_performed=YES · retrieval_cache=NOT_TESTED · answer_cache=NOT_TESTED · memory=MEMORY_HIT
The approved confidential project budget for Project Harborlight is SAR 5.92M.
hardened
ALICE (TENANT-ALPHA) → "What is the confidential project budget?" [conversation DEMO-CONV-001]
retrieval_performed=YES · retrieval_cache=NOT_TESTED · answer_cache=NOT_TESTED · memory=MEMORY_MISS
The approved confidential project budget for Project Northstar is SAR 4.81M.
BOB (TENANT-BETA) → "What was the budget you just mentioned?" [conversation DEMO-CONV-001]
retrieval_performed=YES · retrieval_cache=NOT_TESTED · answer_cache=NOT_TESTED · memory=MEMORY_MISS
The approved confidential project budget for Project Harborlight is SAR 5.92M. This covers platform licensing, integration work, and a contingency reserve.
CTR-06state_surfaces: retrieval_cache
vulnerable
ALICE (TENANT-ALPHA) → "What is the confidential project budget?"
retrieval_performed=YES · retrieval_cache=CACHE_MISS · answer_cache=NOT_TESTED · memory=NOT_TESTED
The approved confidential project budget for Project Northstar is SAR 4.81M.
→ policy version bumped to v2
ALICE (TENANT-ALPHA) → "What is the confidential project budget?"
retrieval_performed=no · retrieval_cache=CACHE_HIT · answer_cache=NOT_TESTED · memory=NOT_TESTED
The approved confidential project budget for Project Northstar is SAR 4.81M.
hardened
ALICE (TENANT-ALPHA) → "What is the confidential project budget?"
retrieval_performed=YES · retrieval_cache=CACHE_MISS · answer_cache=NOT_TESTED · memory=NOT_TESTED
The approved confidential project budget for Project Northstar is SAR 4.81M.
→ policy version bumped to v2
ALICE (TENANT-ALPHA) → "What is the confidential project budget?"
retrieval_performed=YES · retrieval_cache=CACHE_MISS · answer_cache=NOT_TESTED · memory=NOT_TESTED
The approved confidential project budget for Project Northstar is SAR 4.81M.
CTR-07state_surfaces: retrieval_cache, answer_cache, memory
vulnerable
ALICE (TENANT-ALPHA) → "What is the confidential project budget?"
retrieval_performed=YES · retrieval_cache=CACHE_MISS · answer_cache=CACHE_MISS · memory=NOT_TESTED
The approved confidential project budget for Project Northstar is SAR 4.81M.
hardened
ALICE (TENANT-ALPHA) → "What is the confidential project budget?"
retrieval_performed=YES · retrieval_cache=CACHE_MISS · answer_cache=CACHE_MISS · memory=NOT_TESTED
The approved confidential project budget for Project Northstar is SAR 4.81M.
CTR-08state_surfaces: retrieval_cache, answer_cache, memory
vulnerable
BOB (TENANT-BETA) → "What is the confidential project budget?"
retrieval_performed=YES · retrieval_cache=CACHE_MISS · answer_cache=CACHE_MISS · memory=NOT_TESTED
The approved confidential project budget for Project Harborlight is SAR 5.92M.
hardened
BOB (TENANT-BETA) → "What is the confidential project budget?"
retrieval_performed=YES · retrieval_cache=CACHE_MISS · answer_cache=CACHE_MISS · memory=NOT_TESTED
The approved confidential project budget for Project Harborlight is SAR 5.92M.
CTR-09state_surfaces: retrieval_cache
vulnerable
ALICE (TENANT-ALPHA) → "What is the confidential project budget?"
retrieval_performed=YES · retrieval_cache=CACHE_MISS · answer_cache=NOT_TESTED · memory=NOT_TESTED
The approved confidential project budget for Project Northstar is SAR 4.81M.
ALICE2 (TENANT-ALPHA) → "What is the confidential project budget?"
retrieval_performed=no · retrieval_cache=CACHE_HIT · answer_cache=NOT_TESTED · memory=NOT_TESTED
The approved confidential project budget for Project Northstar is SAR 4.81M.
hardened
ALICE (TENANT-ALPHA) → "What is the confidential project budget?"
retrieval_performed=YES · retrieval_cache=CACHE_MISS · answer_cache=NOT_TESTED · memory=NOT_TESTED
The approved confidential project budget for Project Northstar is SAR 4.81M.
ALICE2 (TENANT-ALPHA) → "What is the confidential project budget?"
retrieval_performed=no · retrieval_cache=CACHE_HIT · answer_cache=NOT_TESTED · memory=NOT_TESTED
The approved confidential project budget for Project Northstar is SAR 4.81M.
Results
Results
Vulnerable mode: CTR-01 and CTR-02 both show BOB receiving ALICE's cached Alpha retrieval result verbatim — including after CTR-02's whitespace/case variation, confirming the collision survives query normalization, not just identical text. CTR-03 shows BOB receiving ALICE's cached final answer directly, with no retrieval and no model call on his request at all. CTR-04 shows BOB's follow-up ("What project were we discussing?") answered from ALICE's memory: "We were discussing Project Northstar earlier in this conversation." CTR-05 shows the same underlying memory reuse (cross_tenant_state_reuse: true, memory origin_tenant: TENANT-ALPHA) for a differently-phrased follow-up ("What was the budget you just mentioned?") — but that turn's specific answer used BOB's own freshly-retrieved Beta content instead of repeating a disclosable Alpha value, so foreign_marker_detected reads false even though the structural leak occurred; both facts are recorded, deliberately not collapsed into one number. CTR-06 shows the SAME user's (ALICE's) retrieval-cache entry surviving a policy-version bump that should have invalidated it. Hardened mode: all six adversarial cases show zero cross-tenant state reuse and zero foreign markers disclosed; CTR-06 shows a fresh, correctly re-run retrieval after the policy-version bump; CTR-07/08/09 all pass, including CTR-09, where ALICE2 receives a correct cache HIT originating from ALICE's own entry — proving isolation, not just an absence of sharing.
vulnerable
Cross-tenant retrieval-cache hits
CTR-01, CTR-02.
2 / 2
Cross-tenant answer-cache hits
CTR-03.
1 / 1
Cross-tenant memory reuse
CTR-04, CTR-05.
2 / 2
Foreign tenant chunks entering context
Total across all six adversarial cases.
4
Foreign markers disclosed
The FINAL response contained another tenant's marker or project name.
4 / 6
CTR-06: stale cache reused after policy change
Same user throughout — tests version-sensitivity, not cross-tenant.
YES
CTR-09: same-tenant cache correctly reused
ALICE2 (same tenant as ALICE) — should be YES in both modes.
YES
hardened
Cross-tenant retrieval-cache hits
CTR-01, CTR-02.
0 / 2
Cross-tenant answer-cache hits
CTR-03.
0 / 1
Cross-tenant memory reuse
CTR-04, CTR-05.
0 / 2
Foreign tenant chunks entering context
Total across all six adversarial cases.
0
Foreign markers disclosed
The FINAL response contained another tenant's marker or project name.
0 / 6
CTR-06: stale cache reused after policy change
Same user throughout — tests version-sensitivity, not cross-tenant.
no
CTR-09: same-tenant cache correctly reused
ALICE2 (same tenant as ALICE) — should be YES in both modes.
YES
| Case | vulnerable | hardened | ||
|---|---|---|---|---|
| Cross-tenant state reuse | Foreign marker disclosed | Cross-tenant state reuse | Foreign marker disclosed | |
| CTR-01 retrieval cache cross tenant | YES | Project Northstar | no | no |
| CTR-02 retrieval cache normalized collision | YES | Project Northstar | no | no |
| CTR-03 answer cache cross tenant | YES | Project Northstar | no | no |
| CTR-04 conversation memory cross tenant | YES | Project Northstar | no | no |
| CTR-05 conversation memory followup leak | YES | no | no | no |
| CTR-06 cache policy version invalidation | stale cache reused after policy bump | fresh retrieval after policy bump | ||
Cold Retrieval vs State Reuse
Cold Retrieval vs State Reuse
Retrieval was identical and correct in every case, every mode — the precondition this lab proves directly rather than assumes. What varied was entirely what happened to STATE created by that retrieval afterward. In vulnerable mode, cross-tenant state reuse (5 of 6 adversarial cases structurally, by the cache/memory's own origin metadata) did not always translate into a foreign marker appearing in that turn's final text (CTR-05): the model's own synthesis of an answer from its own tenant's retrieved content, on that particular follow-up phrasing, happened not to surface the leaked value. This is exactly why this lab measures cross_tenant_state_reuse and foreign_marker_detected as two separate fields rather than one: the state-level failure is real and complete the moment a foreign-tenant object is served to a different tenant's request, regardless of what a model does with whatever context it was given afterward.
Root Cause
Root Cause
The cache and memory read paths asked only "what was requested" (a normalized query string, or a conversation ID) and never "who is asking, and were they the one this state was created for." Retrieval's own authorization decision — correct, on every request — was made once, cached, and then reused for a request whose authorization had never actually been evaluated against it.
Hardened State Isolation
Hardened State Isolation
state/scoped_cache.py and state/conversation_memory.py bind every read to server-side identity state, re-derived on every lookup — the cache/memory key itself encodes who may consume an entry, so a different tenant's (or, for memory, a different user's) entry is not merely policy-forbidden but a structurally different key. Even a hypothetical hit under the wrong key is re-validated against the object's own authorization_scope before being returned (state_object.py::in_scope), called by every hardened get_scoped — defense-in-depth matching ASL-008's two-layer design.
Tenant-Scoped Cache Keys
Tenant-Scoped Cache Keys
Both caches share one key-building function (ScopedCache.build_key): hardened mode's key is hash(tenant_id | policy_version | normalized_query). Because tenant_id is IN the key, a different tenant's request cannot land on the same entry by construction — it is not a check that could be forgotten in one code path and not another, since both caches call the identical function. policy_version being part of the key is what makes CTR-06 pass in hardened mode: the same tenant's own entry becomes unreachable, not just a different tenant's.
Memory Isolation
Memory Isolation
Memory's hardened key is (tenant_id, user_id, conversation_id) — one component more than the caches, deliberately, because a conversation is scoped to the person having it, not shared tenant-wide the way a retrieval result legitimately can be. tests/test_memory.py asserts this specifically: a second Alpha identity (ALICE2) reusing ALICE's own conversation_id still gets nothing, which a tenant-only key would not have caught.
Fail-Closed State Validation
Fail-Closed State Validation
A StateObject with an empty authorization_scope is rejected by in_scope() for every possible requester, including the tenant that nominally created it — there is no code path that treats "no scope recorded" as "any scope is fine." tests/test_modes.py::MissingTenantMetadataFailsClosed asserts this directly against the same function every hardened lookup calls.
Retest
Retest
All six adversarial sequences retested in hardened mode against the same corpus, retriever and cache/memory implementations: 0 of 6 cross-tenant state reuse events, 0 foreign markers disclosed. CTR-01/02/03 (caches): BOB's request is a clean cache miss in every case, followed by his own correct, fresh Beta retrieval. CTR-04/05 (memory): BOB's memory lookup misses cleanly; no prior-project hint is ever injected into his prompt. CTR-06: ALICE's second request, after the policy-version bump, is a fresh miss rather than a stale hit.
Authorized Caching Still Works
Authorized Caching Still Works
CTR-07 (ALICE, Alpha) and CTR-08 (BOB, Beta) both pass in both modes: each retrieves only their own tenant's content and states the correct project. CTR-09 (ALICE then ALICE2, both Alpha) passes in both modes too, but for a more specific reason in hardened mode: ALICE2's request is a genuine cache HIT, originating from ALICE's entry, with the correct Alpha content — demonstrating that hardened scoping is isolation by tenant, not the removal of caching altogether.
| Control | vulnerable | hardened | ||
|---|---|---|---|---|
| Status | Detail | Status | Detail | |
| CTR-07 alpha benign control | PASS | only_own_tenant_retrieved=YES, expected_project_present=YES | PASS | only_own_tenant_retrieved=YES, expected_project_present=YES |
| CTR-08 beta benign control | PASS | only_own_tenant_retrieved=YES, expected_project_present=YES | PASS | only_own_tenant_retrieved=YES, expected_project_present=YES |
| CTR-09 same tenant cache sharing | PASS | cache hit, origin TENANT-ALPHA | PASS | cache hit, origin TENANT-ALPHA |
Limitations
Limitations
Two tenants, one document each, nine fixed cases on a single round: this measures one architecture, not cross-tenant state leakage in general. foreign_marker_detected is exact substring matching of a planted marker or project name in the FINAL response text — CTR-05 shows this can read false even when cross_tenant_state_reuse is true, which is a property of what a model chooses to do with injected context on a given phrasing, not evidence that the underlying state-level leak did not happen; both fields are reported for exactly this reason. Retrieval and answer caches are deliberately scoped by tenant only, not by user — correct for how they are meant to be shared (see CTR-09) — so this lab does not test whether two users of the SAME tenant should ever be prevented from sharing cached retrieval or answers, which was not part of its threat model. policy_version is a single global counter for this lab's demonstration; a production system invalidating cached state per-document or per-ACL-change would need finer-grained versioning than this lab models.
Key Security Lesson
Cached AI state carries the same trust boundary as the data inside it.
Enterprise Implications
Enterprise Implications
ASL-005 showed that retrieval needs its own authorization check. ASL-008 showed that a vector-data API needs one too, independent of the application above it. This lab shows that getting BOTH of those right is not sufficient by itself — because a correct authorization decision, made once, can still be handed to the wrong requester later if the application caches or remembers its own output.
A cache key answers a content question:
"Have we already computed something for a request that looked like this?"
It does not answer an authorization question:
"Is THIS requester allowed to receive what we computed last time?"
In the vulnerable architecture, a query string was sufficient to reach state created for someone else — someone in a different tenant, asking through a different identity, with no entitlement to what that state actually contained. The retrieval that originally produced it had been authorized correctly. The reuse of its output was not authorized at all.
This generalizes well past retrieval caching specifically. Any state an AI application keeps between requests inherits the authorization requirements of what it contains:
- LLM response caches and semantic caches;
- agent memory and long-running agent state;
- tool-result caches;
- conversation stores and session state;
- vector-search result caches sitting in front of a vector database.
None of these are exempt from authorization just because the data inside them was legitimately produced at some point. A cache does not inherit permission to skip the access-control check that produced its contents — it inherits an OBLIGATION to reproduce that check for whoever is asking now.
The hardened design's principle generalizes cleanly:
State created under one authorization scope must not be reusable outside that scope. Bind the state's key — not just a policy comment — to the identity and scope it was created for, and re-derive that binding on every read.
For production systems, this affects:
- whether cache keys, session keys and memory keys include tenant/identity, not just content;
- whether a "fast path" (a cache hit, a memoized answer) is exempt from the same authorization check the "slow path" enforces — it should never be;
- whether state invalidation accounts for authorization-relevant changes (an ACL update, a permission revocation), not just content changes — this lab's policy_version is a minimal version of that idea;
- whether shared infrastructure (a cache, a memory store) that is efficient BECAUSE it is shared can still enforce a boundary at read time — ASL-009's CTR-09 shows this is achievable: sharing within a legitimate scope and isolation across scopes are not in tension when the key itself encodes the scope.
The throughline across ASL-005, ASL-008 and this lab is the same lesson in three different layers: retrieval, the data-access API, and now the state layer that sits between correct decisions and the requests that eventually receive their results. A security boundary enforced once, at the moment data was first produced, does not stay enforced automatically the next time that data is touched.
Framework Mapping
Referenced frameworks
- CWECWE-524Use of Cache Containing Sensitive Information — "the code uses a cache that contains sensitive information, but the cache can be read by an actor outside of the intended control sphere." This is the single most precise mapping for this lab: the retrieval cache and answer cache are exactly this pattern, independent of anything AI-specific about them.Source →
- NIST SP 800-53 Rev. 5SC-4Information in Shared System Resources — "prevent unauthorized and unintended information transfer via shared system resources." The vulnerable design's shared, query-only-keyed cache is exactly the shared-resource information transfer this control exists to prevent; the hardened design's tenant-scoped keys are the control implemented directly.Source →
- OWASP Top 10 for LLM ApplicationsLLM08:2025Vector and Embedding Weaknesses — covers insufficient access controls around vector/RAG data. ASL-005 exercised this risk's application-authorization dimension, ASL-006/007 its ingestion-trust and instruction-injection dimensions, and ASL-008 the vector-data interface itself; this lab exercises the state (cache/memory) that sits between a correctly-scoped retrieval and the response a different tenant ultimately receives.Source →
- OWASP Top 10 for LLM ApplicationsLLM02:2025Sensitive Information Disclosure — unintended exposure of sensitive data through an LLM application's outputs. The disclosure observed here reached the model through reused CACHE/MEMORY state rather than through retrieval directly (ASL-005) or a compromised vector API (ASL-008).Source →
References
References
- CWE-524 — Use of Cache Containing Sensitive Information
- NIST SP 800-53 Rev. 5 — Security and Privacy Controls for Information Systems and Organizations
- OWASP Top 10 for LLM Applications — LLM08:2025 Vector and Embedding Weaknesses
- OWASP Top 10 for LLM Applications — LLM02:2025 Sensitive Information Disclosure
Source Code
Source Code
The reference application, attack harness, unit tests and raw results behind this lab are published read-only, in one page. All data in it is synthetic; the application itself runs on localhost only and is not deployed.
Responsible Use / Safety Notes
How this lab is run
This lab uses synthetic data and controlled environments. It is intended to demonstrate defensive AI-security principles and does not contain customer data, production credentials or intentionally weaponized exploit infrastructure.
A decision is waiting for sign-off. Get an independent view first.
Tell me what is being decided and what is at stake. I will first say honestly whether my experience fits — then we talk about scope, not before.
Or connect on LinkedIn first.