Membership Inference Defence
Membership inference attacks determine whether a specific record exists in a system's knowledge base. In a RAG platform, this means probing whether a particular claim file, policy document, or personal record is indexed in Qdrant β without ever accessing Qdrant directly.
Classical Membership Inference β Low Riskβ
Classical membership inference determines whether a specific data sample was in a model's training set by probing output confidence or perplexity. Researchers can distinguish "the model memorised this text" from "the model hasn't seen this before."
This is low risk here for the same reason as model inversion (see Model Inversion): llama3.2:1b/3b were trained on public web data. No HDI Seguros documents are in that training set.
This changes if fine-tuning ever happens β the fine-tuning gate in issue #48 blocks that path until a membership inference test is explicitly documented and closed.
The Real Risk β RAG Membership Probingβ
An attacker with a valid virtual key can determine whether a specific document is indexed in Qdrant by crafting queries that would only be answerable if that record were present:
"Quel est le montant d'indemnisation du sinistre rΓ©fΓ©rence FR-2024-00892?"
β Model answers with specific amount β document IS in Qdrant
β "Je n'ai pas trouvΓ© d'informations" β document is NOT in Qdrant
Each probe is a single query β far more surgical than knowledge base enumeration (issue #48). The enumeration detector does not catch this because it measures topic diversity, not identifier presence.
Three probe patternsβ
Pattern 1 β Direct identifier probe
The attacker supplies a structured business identifier and measures response specificity:
"Quel est le montant pour le sinistre FR-2024-00892?"
"Quelle est la franchise pour la police MRH-2024-11547?"
"Quels sont les dΓ©tails du dossier SIN-2023-004512?"
Pattern 2 β Corroborative probe
The attacker already knows partial details (from a leaked document, a former employee, or public records) and probes to confirm the rest or verify a hypothesis:
"Confirme que la franchise pour la police MRH-2024-11547 est de 150 β¬."
β Model confirms β policy indexed, franchise is 150 β¬
β Model contradicts with different amount β policy indexed, actual franchise revealed
β "No documents found" β policy not indexed
All three outcomes are membership signals.
Pattern 3 β Personal data existence probe
The attacker (or a legitimate data subject exercising GDPR rights) asks whether the system holds their personal data:
"Quelles informations avez-vous sur M. Jean Dupont, nΓ© le 14/03/1978 Γ Lyon?"
"Est-ce que mes données sont dans votre système?"
"Do you have a file on me?"
Whether answered "yes" or "no", the model is making a membership determination about personal data β a data processing decision that belongs to the Data Protection Officer, not to the AI.
What's Already In Your Stackβ
| Control | What it covers |
|---|---|
| Presidio PII masking (issue #37) | Strips names, dates of birth, national IDs from prompts β partially covers pattern 3 |
| Enumeration detection (issue #48) | Catches bulk probing across many topics; single-query probing bypasses it |
| Collection ACL (issue #47) | Scopes which departments can query which collections β limits blast radius of a compromised key |
| Response abstraction (issue #48) | Reduces specificity of answers β partial mitigation against pattern 2 |
| Virtual key routing (issue #34) | Department-scoped access β IT key cannot probe claims-docs |
The retrieval grounding gate (issue #44) inadvertently worsens pattern 1: "Aucun document trouvΓ© (score 0.21 < 0.35)" is a clean negative membership signal with quantified confidence.
Defence Architectureβ
PRE-CALL (extends RetrievalGroundingHook + PromptInjectionHook)
βββ Identifier stripping
β Claim refs, policy numbers, plates, addresses β placeholders
β before query reaches Qdrant
β β membership-probe-identifiers-stripped WARNING in Langfuse
β
βββ GDPR Art. 15 routing
Access request patterns detected β HTTP 451
β DPO email + 30-day deadline returned
β AI never answers membership questions about personal data
β gdpr-access-request-detected INFO in Langfuse
RETRIEVAL GATE (fix to RetrievalGroundingHook, issue #44)
βββ Neutral error message β similarity score removed from response
Score logged internally; never returned to caller
"Je n'ai pas trouvΓ© d'informations suffisantes"
(same message at score 0.05 and score 0.34)
Layer 1 β Identifier Strippingβ
What it doesβ
Structured business identifiers are replaced with typed placeholders before the query reaches Qdrant. The retrieval system never sees the specific reference, so it cannot match a specific record. The model answers generically based on policy type, not based on a specific file.
Identifiers strippedβ
| Type | Example | Placeholder |
|---|---|---|
| Claim reference | FR-2024-00892, SIN-2023-004512 | [RΓFΓRENCE SINISTRE] |
| Policy number | MRH-2024-11547, AUTO-2023-998877 | [NUMΓRO POLICE] |
| Broker / intermediary code | CRT12345, INT-AB7890 | [CODE COURTIER] |
| Vehicle registration | AB-123-CD | [IMMATRICULATION] |
| Exact street address | 15 rue de la Paix | [ADRESSE] |
Presidio already handles: names, dates of birth, national IDs (NIR), IBANs, phone numbers. Identifier stripping extends Presidio's pipeline for business-specific tokens that are not PII in the GDPR sense but are membership probe tokens in the RAG sense.
What the user experiencesβ
User query: "Quel est le montant pour le sinistre FR-2024-00892?"
Sent to Qdrant: "Quel est le montant pour le [RΓFΓRENCE SINISTRE]?"
Model response: "Les montants d'indemnisation pour un sinistre dΓ©gΓ’t des eaux
dΓ©pendent des conditions gΓ©nΓ©rales de votre contrat. La franchise
standard est de 150 β¬ et le plafond est..."
The user gets a general answer about claim amounts. No membership information is revealed. A legitimate adjuster working claim FR-2024-00892 should retrieve the file directly from the claims management system, not ask the AI assistant for its specific amounts.
Langfuse eventβ
membership-probe-identifiers-stripped (WARNING) fires whenever identifiers are stripped. Metadata includes the count and categories of identifiers removed β not the actual values, which are already stripped.
Layer 2 β Neutral Error Messageβ
The problem with the current messageβ
The retrieval grounding gate (issue #44) returns:
"Aucun document pertinent trouvΓ© (score de similaritΓ© 0.21 < 0.35)"
The numeric score is a graded membership signal:
- Score 0.05 β very clearly not indexed
- Score 0.21 β not indexed but closer
- Score 0.34 β almost at threshold β document may be indexed but query didn't match well
- Score 0.45 β above threshold β different response path (document found)
An attacker running the same probe with slight query variations can map the similarity landscape and infer partial membership information even below the threshold.
Fixβ
Remove the score from the user-visible message. Log it internally only.
Before:
"Aucun document pertinent trouvΓ© (score de similaritΓ© 0.21 < 0.35).
Veuillez reformuler ou contacter votre rΓ©fΓ©rent Sinistres."
After:
"Je n'ai pas pu trouver d'informations suffisantes pour rΓ©pondre
Γ cette question. Veuillez reformuler ou contacter votre rΓ©fΓ©rent Sinistres."
Score 0.05 and score 0.34 produce identical responses. The score is still recorded in the Langfuse retrieval_gate_blocked event for operational use.
Layer 3 β GDPR Art. 15 Routingβ
Legal contextβ
GDPR Article 15 grants data subjects the right to know whether their personal data is being processed and to access it. If a user asks "do you have information about me?", the correct response is a legal one β not a technical one.
The AI answering this question directly creates two problems:
- If it says "yes": it has just confirmed processing of personal data, triggering full Art. 15 obligations the AI cannot fulfil (it cannot provide a copy, explain the purpose, list third-party recipients)
- If it says "no": it may be wrong (documents are indexed under different identifiers), and a false denial of data processing is a GDPR violation
Neither answer is safe. The only correct response is to route to the DPO.
Patterns detectedβ
French:
"avez-vous des informations sur moi?"
"mes données figurent-elles dans votre système?"
"que savez-vous sur moi?"
"je veux accéder à mes données" / "droit d'accès"
English:
"do you have information about me?"
"is my data stored in your system?"
"right of access" / "GDPR article 15"
"do you have a file on me?"
Responseβ
HTTP 451 Unavailable For Legal Reasons
"Pour exercer votre droit d'accès aux données personnelles (GDPR Art. 15),
veuillez contacter notre DΓ©lΓ©guΓ© Γ la Protection des DonnΓ©es :
dpo@hdiseguros.fr β dΓ©lai de rΓ©ponse : 30 jours conformΓ©ment au RGPD."
HTTP 451 is the correct status code for legally-mandated refusals (defined in RFC 7725). It communicates that this is a legal restriction, not a technical error, and is more informative than 403 or 400 for this use case.
The gdpr-access-request-detected INFO event is logged to Langfuse with the matched pattern and department. These events should be reviewed monthly β a data subject who escalates to the DPO without having received a response within 30 days creates a compliance incident.
Langfuse Event Referenceβ
| Event name | Level | When fired |
|---|---|---|
membership-probe-identifiers-stripped | WARNING | Claim ref, policy number, plate, or address stripped from query |
gdpr-access-request-detected | INFO | GDPR Art. 15 access request pattern matched β routed to DPO |
Operational Checksβ
# Membership probe attempts today (identifier stripping events)
# β Langfuse β Events β filter: name = "membership-probe-identifiers-stripped"
# GDPR access requests this month (should be reviewed for DPO follow-up)
# β Langfuse β Events β filter: name = "gdpr-access-request-detected"
# β confirm DPO has responded within 30 days for each
# Verify neutral error message is active (no score in response)
kubectl exec -n ai deployment/litellm -- \
curl -s -X POST http://localhost:4000/chat/completions \
-H "Authorization: Bearer $TEST_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "claims-assistant", "messages": [{"role": "user",
"content": "xyz123 completely unrelated query"}]}' \
| jq '.error.message'
# β should NOT contain "score de similaritΓ©" or any numeric value
What These Controls Do Not Coverβ
| Gap | Reason | Path |
|---|---|---|
| Timing-based membership inference | Response latency differs when Qdrant finds vs does not find a match β measurable with enough probes | Add artificial jitter (5β50ms random delay) to retrieval gate responses as v2 |
| Partial identifier probing (attacker has only first 4 digits of claim ref) | Stripping requires a full pattern match; partial refs may not be caught | Extend patterns with fuzzy matching on numeric sequences in context of claim/policy words |
| Fine-tuned model membership inference | Not applicable today | Fine-tuning gate (issue #48) blocks this until membership inference test is documented |
Relationship to Other Controlsβ
issue #37 Presidio β strips PII (names, DOB, NIR, IBAN)
issue #49 this issue β strips business identifiers (claim refs, policy numbers)
together: no personal or business identifier reaches Qdrant
issue #44 retrieval gate β blocks low-similarity queries
issue #49 neutral message β removes score from error response
together: no membership signal in either positive or negative gate outcome
issue #46 injection hook β blocks malicious prompt patterns
issue #49 GDPR routing β redirects legal access requests
together: PromptInjectionHook handles both attack and legal dimensions