How to Structure Data for AI Without Creating Security Nightmares
Balance AI context with security: structured data, sanitization, RAG, and least-privilege. Practical patterns for safe AI without data exfiltration risks.
AI systems face a fundamental tension: they need rich context to provide useful responses, but every additional piece of context expands the attack surface. Send too little data and the AI is useless. Send too much and you’ve created a data exfiltration risk.
Traditional security principles like encryption-at-rest and access control still apply, but they’re insufficient for AI systems. You need new patterns specifically designed for probabilistic systems that process natural language at scale.
This post documents practical approaches for structuring data that AI models can effectively use while maintaining security boundaries.
The AI Context Problem
Why Context Matters
AI models are stateless—they have no memory between requests. Everything the model needs to know must be included in each prompt. This creates pressure to include more data:
Minimal context example:
prompt = "How do I reset my password?"
AI response: Generic instructions applicable to any system.
Rich context example:
prompt = f"""
User Question: How do I reset my password?
User Context:
- Name: {customer.name}
- Email: {customer.email}
- Account ID: {customer.account_id}
- Account Status: {customer.status}
- Last Login: {customer.last_login}
- Recent Orders: {customer.recent_orders}
- Support History: {customer.support_tickets}
"""
AI response: Personalized, specific, genuinely helpful.
But now the prompt contains:
- ❌ Personally Identifiable Information (PII)
- ❌ Account details that could enable account takeover
- ❌ Purchase history exposing customer behavior
- ❌ Support history potentially containing sensitive issues
If prompt injection succeeds, all this data leaks.
The Attack Surface Expansion
Every piece of data in a prompt represents potential exposure:
- Prompt injection can trick the model into outputting embedded data
- Model provider sees all prompt content (unless using local models)
- Logging systems may capture prompts containing sensitive information
- Debugging/monitoring tools expose prompts to engineers
- Prompt caching may store sensitive data longer than intended
The more context you provide, the more ways attacks can succeed.
Principle 1: Make Structure Explicit
AI models understand structured data better than freeform text. Explicit structure also gives you precise control over what’s included.
Bad: Unstructured Data Dump
# Don't do this
prompt = "Here's all the user info: " + json.dumps(user_record)
Problems:
- Model must parse arbitrary JSON
- You’ve included everything, not just what’s needed
- No clear boundaries between data types
- Hard to audit what was sent
Good: Hierarchical Markdown Structure
# Do this
prompt = f"""
## User Information
- ID: {user.id}
- Role: {user.role}
- Department: {user.department}
## Request
{sanitize(user_request)}
## Context
- Timestamp: {timestamp}
- Session ID: {session.id}
- Previous Action: {previous_action}
"""
Benefits:
- Clear hierarchy from general to specific
- Explicit sections you can selectively include/exclude
- Easy to redact specific sections (e.g., remove PII for certain tasks)
- Audit trail shows exactly what was sent
- Model can focus on relevant sections
Semantic Tagging
Use tags to clarify the purpose of each data element:
<SYSTEM_PROMPT>
You are a secure code review assistant.
Never execute code. Only analyze for vulnerabilities.
</SYSTEM_PROMPT>
<USER_REQUEST>
Review this authentication function for security issues.
</USER_REQUEST>
<CODE classification="internal">
def login(username, password):
query = f"SELECT * FROM users WHERE name='{username}'"
cursor.execute(query)
return cursor.fetchone()
</CODE>
<SECURITY_CONTEXT>
This code handles user authentication for internal admin portal.
OWASP Top 10 compliance required.
</SECURITY_CONTEXT>
Benefits:
- Clear separation between instructions and user input (helps with prompt injection defense)
- Classification labels enable access control checks before sending
- Purpose tags help model understand context type
- Easier to parse and validate programmatically
Note: Tags alone don’t prevent prompt injection, but they make attacks harder and provide structure for security controls.
Principle 2: Least Context Necessary
Only include data the AI needs for this specific task. This is the least-privilege principle applied to AI context.
Bad: Over-Sharing
# Sends entire customer database to AI
customers = database.query("SELECT * FROM customers")
prompt = f"Summarize feedback from these customers: {customers}"
Problems:
- Massive privacy violation
- Huge token cost
- Model overwhelmed by irrelevant data
- Catastrophic if prompt leaks
Good: Minimal Scope
# Sends only relevant subset
feedback = database.query("""
SELECT feedback_text, category, date
FROM customer_feedback
WHERE category = 'product' AND date > DATE_SUB(NOW(), INTERVAL 30 DAY)
LIMIT 100
""")
prompt = f"Summarize recent product feedback: {sanitize_feedback(feedback)}"
Benefits:
- Only necessary data exposed
- Reduced token cost
- Model focuses on relevant information
- Limited blast radius if compromised
Principle 3: Redaction and Sanitization
Strip sensitive data before sending to AI.
Practical Sanitization Example
Original email:
From: [email protected]
Subject: Billing Issue - Order #A-98765
Hi Support,
My credit card ending in 4567 was charged $299.99 twice for
order #A-98765. My customer ID is C-123456.
Please refund the duplicate charge to card ending 4567.
Thanks,
John Doe
SSN: 123-45-6789 (for verification)
Sanitized for AI:
From: [USER_EMAIL]
Subject: Billing Issue - Order #[ORDER_ID]
Hi Support,
My payment method ending in [REDACTED] was charged [AMOUNT]
twice for order #[ORDER_ID]. My customer ID is [CUSTOMER_ID].
Please refund the duplicate charge.
Thanks,
[USER_NAME]
Implementation:
import re
def sanitize_for_ai(text):
"""Remove sensitive data before sending to AI"""
# Redact email addresses
text = re.sub(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b',
'[USER_EMAIL]', text)
# Redact credit card numbers (last 4 digits)
text = re.sub(r'\b\d{4}\b', '[REDACTED]', text)
# Redact SSN
text = re.sub(r'\b\d{3}-\d{2}-\d{4}\b', '[SSN_REDACTED]', text)
# Redact dollar amounts (preserve for context but anonymize scale)
text = re.sub(r'\$[\d,]+\.?\d*', '[AMOUNT]', text)
# Replace actual names (requires NER or lookup)
text = replace_known_names(text, '[USER_NAME]')
# Replace order IDs with generic tokens
text = re.sub(r'#[A-Z]-\d+', '#[ORDER_ID]', text)
return text
AI can still:
- Understand the issue (duplicate charge)
- Generate appropriate response
- Route to correct team (billing)
- Provide helpful troubleshooting
AI cannot:
- See real email address
- Access credit card details
- View customer name
- Extract order numbers for unauthorized lookups
Result: Significantly reduced privacy risk while maintaining functionality.
Principle 4: Data Classification and Routing
Route data to appropriate models based on sensitivity.
| Classification | Model Type | Rationale |
|---|---|---|
| Public | Cloud AI (Claude, GPT-4) | Best performance, no privacy concern |
| Internal | Cloud AI with DPA/BAA | Business Associate Agreement for compliance |
| Confidential | Local/on-prem model | Data never leaves your control |
| Secret/Regulated | No AI processing | Human-only, too sensitive for AI |
Implementation Example
def route_code_review(code_file):
"""Route code review to appropriate model based on classification"""
classification = classify_file(code_file) # Use data classification system
if classification == "public":
# Open-source code, use best available model
return claude_api.review(code_file)
elif classification == "internal":
# Proprietary but not critical, cloud with Data Processing Agreement
if has_signed_dpa("openai"):
return openai_api.review(code_file)
else:
return local_model.review(code_file)
elif classification == "confidential":
# Trade secrets, critical IP - local model only
return local_model.review(code_file)
elif classification in ["secret", "top-secret"]:
# Classified information, government secrets
return {
"status": "rejected",
"reason": "Classification too high for AI processing",
"recommendation": "Manual review required"
}
else:
raise ValueError(f"Unknown classification: {classification}")
Security through routing: Sensitive data never touches cloud providers.
Retrieval-Augmented Generation (RAG): Security-First Pattern
RAG solves the context problem by retrieving only relevant data instead of loading everything into prompts.
Traditional Approach (Insecure)
# BAD: Load all documents
all_company_docs = load_all_documents() # Thousands of documents
prompt = f"""
Question: {user_question}
Company Knowledge Base:
{all_company_docs}
"""
response = ai.query(prompt)
Problems:
- User might not have access to all documents
- Massive token cost
- Prompt contains far more than needed
- Privacy violations if docs contain PII
- Slow, expensive, insecure
RAG Approach (Secure)
# GOOD: Retrieve only relevant, authorized documents
relevant_docs = vector_db.search(
query=user_question,
filters={
"accessible_by": user.id, # Access control filter
"classification": ["public", "internal"] # Exclude confidential
},
limit=5 # Only top 5 most relevant
)
# Sanitize retrieved documents
sanitized_docs = [sanitize_document(doc) for doc in relevant_docs]
prompt = f"""
Question: {user_question}
Relevant Context (user-authorized):
{sanitized_docs}
"""
response = ai.query(prompt)
# Log retrieval for audit
log_document_access(user.id, [doc.id for doc in relevant_docs])
Security benefits:
- Least privilege: AI only sees documents user can access
- Access control: Search respects user permissions automatically
- Audit trail: Logged which documents were accessed
- Minimal context: Only relevant documents included
- Cost-efficient: Dramatically fewer tokens
RAG Best Practices
1. Chunk Documents Appropriately
Too large chunks: AI gets irrelevant context, wastes tokens Too small chunks: AI lacks context to answer effectively Sweet spot: 500-1000 tokens per chunk
def chunk_document(document, chunk_size=800, overlap=100):
"""
Split document into overlapping chunks for RAG
"""
chunks = []
current_chunk = []
current_size = 0
for paragraph in document.paragraphs:
para_tokens = count_tokens(paragraph)
if current_size + para_tokens > chunk_size:
# Save current chunk
chunks.append({
"text": "\n\n".join(current_chunk),
"metadata": extract_metadata(current_chunk)
})
# Start new chunk with overlap from previous
overlap_text = current_chunk[-1] if current_chunk else ""
current_chunk = [overlap_text, paragraph]
current_size = count_tokens(overlap_text) + para_tokens
else:
current_chunk.append(paragraph)
current_size += para_tokens
# Add final chunk
if current_chunk:
chunks.append({
"text": "\n\n".join(current_chunk),
"metadata": extract_metadata(current_chunk)
})
return chunks
Overlap is critical: Ensures concepts spanning chunk boundaries aren’t lost.
2. Embed Access Control in Metadata
# When indexing documents
chunk_metadata = {
"chunk_id": "doc-547_chunk-12",
"source_document": "security_policy.pdf",
"classification": "internal",
"accessible_by_roles": ["security_team", "management", "employees"],
"accessible_by_users": ["user123", "user456"], # Specific user grants
"created_date": "2024-11-01",
"last_modified": "2025-01-15",
"content_type": "policy_document"
}
vector_db.insert(
vector=chunk_embedding,
metadata=chunk_metadata
)
At query time:
# Retrieve only authorized chunks
results = vector_db.search(
query_vector=embed(user_question),
filters={
"$or": [
{"accessible_by_roles": {"$in": user.roles}},
{"accessible_by_users": user.id}
],
"classification": {"$in": ["public", "internal"]} # User's clearance
},
top_k=5
)
Result: Users retrieve only chunks they’re authorized to see. Access control happens at data layer, not application layer.
3. Sanitize Retrieved Content
Even if user is authorized, sanitize before sending to AI:
def sanitize_chunk(chunk, requesting_user):
"""
Sanitize chunk even after authorization check
"""
text = chunk["text"]
# Redact PII even from authorized documents
text = redact_ssn(text)
text = redact_credit_cards(text)
text = redact_api_keys(text)
# Add provenance watermark
text += f"\n\n[Source: {chunk['metadata']['source_document']}]"
text += f"\n[Retrieved for user {requesting_user.id} at {datetime.now()}]"
# Check for additional redaction rules
if chunk["metadata"].get("contains_financial_data"):
text = redact_financial_details(text)
return {
"text": text,
"source": chunk["metadata"]["source_document"],
"classification": chunk["metadata"]["classification"]
}
Defense in depth: Authorization alone isn’t enough. Always sanitize.
Practical Example: Secure AI Code Review System
Putting it all together:
def secure_code_review_pipeline(code_file, requesting_user):
"""
Complete secure pipeline for AI code review
"""
# Step 1: Classify the code
classification = classify_code(code_file)
# Step 2: Check user authorization
if not user_authorized_for_classification(requesting_user, classification):
raise PermissionError(f"User not authorized for {classification} code")
# Step 3: Route to appropriate model
if classification in ["secret", "top-secret"]:
return {"error": "Classification too high for AI", "manual_review": True}
elif classification == "confidential":
model = local_model_provider
else:
model = cloud_model_provider
# Step 4: Structure the code for AI
structured_prompt = f"""
# Code Review Request
## Metadata
- File: {code_file.path}
- Classification: {classification}
- Language: {code_file.language}
- Author: {code_file.author}
- Date: {code_file.date}
## Review Criteria
- Security vulnerabilities (SQL injection, XSS, command injection)
- Authentication/authorization flaws
- Sensitive data exposure
- Cryptographic issues
## Code
```{code_file.language}
{sanitize_code(code_file.content)}
Instructions
Provide security-focused review. Flag vulnerabilities as HIGH/MEDIUM/LOW. Include OWASP references where applicable. """
# Step 5: Query AI model
response = model.query(structured_prompt)
# Step 6: Filter AI output for any leaked sensitive data
filtered_response = filter_sensitive_output(response)
# Step 7: Log interaction for audit
log_ai_interaction({
"user": requesting_user.id,
"file": code_file.path,
"classification": classification,
"model": model.name,
"timestamp": datetime.now(),
"tokens_used": count_tokens(structured_prompt) + count_tokens(response)
})
return filtered_response
**Security layers:**
1. ✅ Classification-based routing
2. ✅ User authorization check
3. ✅ Structured prompt with clear sections
4. ✅ Code sanitization before sending
5. ✅ Output filtering to catch leaks
6. ✅ Comprehensive audit logging
## Key Takeaways
1. **Structure data explicitly** - Use markdown, tags, and hierarchy for clarity and control
2. **Least context necessary** - Only include data AI needs for specific task
3. **Classify and route** - Sensitive data uses local models or no AI at all
4. **Use RAG for large datasets** - Retrieve only relevant, authorized documents
5. **Sanitize aggressively** - Redact PII and sensitive data before sending to AI
6. **Embed access control** - Authorization at data layer, not just application layer
7. **Log everything** - Audit trail for compliance and forensic analysis
8. **Defense in depth** - Authorization + sanitization + output filtering + monitoring
**Make data AI-readable AND secure.** It requires intentional design, but it's achievable with the right patterns.
The alternative—either crippling AI usefulness with too little context, or exposing massive amounts of sensitive data—is unacceptable. These patterns provide the middle ground: AI systems that are both useful and secure.