Cross-Tenant Leakage: Break RAG Metadata Isolation and Exfiltrate Another Tenant's Contract
Hands-on lab · IDE in your browser

Cross-Tenant Leakage: Break RAG Metadata Isolation and Exfiltrate Another Tenant's Contract

Attack the multi-tenant isolation of a real Retrieval-Augmented Generation assistant.

Time
70 min
Checked steps
8
Level
Advanced
Setup
None
Part of the AI Red Teaming Course
Read step 1

Hands-on labs require Pro · $29.99/mo · cancel anytime

Lab cockpit70 min · 8 stepsSession running
6 / 8 steps passingHarden 3: allow-list the render sink · step 7 of 8
dvrag.py▶ Run✓ Check
# Microsoft 365 user's own data. There is NO secret in the system prompt; the# system prompt is an ordinary support prompt.CURRENT_TENANT = os.environ.get("DVRAG_TENANT", "globex") # FIX (Bug 2 defense in depth): the only tenant IDs this deployment knows. Any# tenant value not in this set is rejected before it reaches the filter, so a# crafted value carrying a quote and a boolean cannot break out of the predicate.KNOWN_TENANTS = {"globex", "initech", "public"}   
TerminalOutput

The job

You are a Globex-scoped caller against DV-RAG-Support, ACME Cloud's multi-tenant customer-support assistant. Every document carries a tenant, and the retriever filters to your tenant plus public. Two stacked isolation bugs let you read another tenant's confidential contract: the retriever trusts a caller-supplied tenant scope, and it builds its metadata filter by string concatenation. You read Initech's negotiated pricing, chain the read into the markdown-image sink to exfiltrate it to your listener, then harden the pipeline so both bugs and the sink are closed.

8 steps, each checked when you finish it

A check runs your work at the end of every step. Hints and the full solution are there if you get stuck.

  1. 1

    Recon: confirm tenant isolation and find the two bugs

    DV-RAG-Support is multi-tenant.

  2. 2

    Scope abuse: name another tenant in the request

    The first isolation bug is the simplest and the most common one in real systems: the tenant comes from the request, not the session.

  3. 3

    Filter injection: break out of the metadata predicate

    Bug 1 worked because the scope was caller-supplied.

  4. 4

    Exfil chain: leak the cross-tenant value through the image sink

    Reading the contract is the access-control finding.

  5. 5

    Harden 1: derive the tenant scope server-side

    Now switch sides and fix the bugs one at a time.

  6. 6

    Harden 2: validate tenant values against an allow-list

    Server-side scope closed Bug 1.

  7. 7

    Harden 3: allow-list the render sink

    The two isolation bugs are closed, but the exfil channel is still open as defense in depth.

  8. 8

    Verify: full regression battery against the hardened pipeline

    A fix only counts when you re-run every exploit against it and watch each one fail.

Step 1 as it appears in the lab

The lab’s own text. The hint and the solution stay inside the lab.

Step 1: Recon: confirm tenant isolation and find the two bugs

DV-RAG-Support is multi-tenant. Every document carries a tenant in its frontmatter, and the retriever filters results to the caller's tenant plus public. You are a Globex-scoped caller. By default you should see only globex and public documents.

Somewhere in the corpus is the prize: kb_initech-contract.md (tenant: initech, confidential), with Initech's negotiated 22% discount and private pricing. A Globex caller must never see it. Your job across this lab is to read it anyway, then exfiltrate it.

Do this
  1. Build the index and ask a normal Globex question:
    • hit Run, or: python3 dvrag.py --build then python3 dvrag.py "Who is my billing contact?"
  2. Read retrieve() carefully and find two isolation bugs:
    • Bug 1 (broken access control). tenant_scope is a parameter the caller passes in. The tenant should come from the authenticated session, never from the request. What happens if the caller names another tenant?
    • Bug 2 (filter-string injection). The filter is built by raw string concatenation: expr = " or ".join(f'tenant == "{t}"' ...). What happens if a tenant value itself contains a quote and a boolean? This is the metadata-filter analogue of SQL injection.

Pass criteria

A default Globex query returns no initech rows (isolation holds when you do not abuse it), and the Initech contract exists in the corpus as the cross-tenant prize you will steal in the next steps.

Provided for you:kb_account-recovery.mdkb_account.mdkb_billing.mdkb_getting-started.mdkb_globex-runbook.mdkb_initech-contract.mdkb_security-sso.mdkb_shipping.mdkb_status-incidents.mdlistener.py

Frequently asked questions

Do I need a machine-learning background?

No. The core of this lab is access control, not model internals. You read a retriever, find two isolation bugs, and exploit them. The exfil step reuses a markdown-image side channel that needs only an understanding of HTTP GETs.

What is the metadata-filter injection bug?

The retriever builds its tenant filter by concatenating strings, so a tenant value carrying a quote and a boolean breaks out of the predicate, the same way an unparameterized SQL query is injectable. You craft such a value and collapse tenant isolation even when the scope is meant to be locked.

How is the cross-tenant exfiltration demonstrated?

You retrieve another tenant's confidential record, then chain it into the EchoLeak markdown-image sink: the assistant echoes a confidential identifier into an image URL that the client auto-loads, firing an HTTP request to an in-pod listener. The lab grades deterministically on the identifier reaching a loaded URL and the listener log.

What you'll do in this lab

This is a hands-on offensive-security lab on multi-tenant RAG isolation. You attack DV-RAG-Support, a customer-support assistant backed by a Milvus vector store and NVIDIA llama-nemotron-embed-vl-1b-v2 embeddings, where documents are partitioned by tenant. Acting as a Globex-scoped caller, you exploit two stacked bugs in a single retriever: a caller-controlled tenant scope (broken access control, where the tenant comes from the request rather than the authenticated session) and a metadata filter built by raw string concatenation (the metadata-filter analogue of SQL injection).

Either bug lets you read Initech's confidential contract and its negotiated pricing. You then chain the cross-tenant read into the EchoLeak markdown-image channel: a planted public audit policy coaxes the assistant into echoing a confidential field into an image URL the client auto-loads, exfiltrating it to an in-pod listener. Finally you switch to defense and harden the pipeline: derive the tenant scope server-side, validate tenant values against an allow-list, and allow-list the render sink, all without breaking legitimate answers.