TL;DR
Sanity is a headless CMS with a powerful query language (GROQ). Use read-only tokens for public content fetching. Keep write tokens server-side only. Be careful with GROQ queries that include user input - use parameters instead of string interpolation. Configure CORS properly to restrict which domains can access your content API. Verify webhook signatures before processing.
Why Sanity Security Matters for Vibe Coding
Sanity provides a flexible content backend that many developers use for blogs, e-commerce, and applications. When AI tools generate Sanity integration code, they often create working queries but may expose tokens or create GROQ injection vulnerabilities.
API Token Management
# .env.local (never commit)
# Project configuration (safe for client)
NEXT_PUBLIC_SANITY_PROJECT_ID=your-project-id
NEXT_PUBLIC_SANITY_DATASET=production
# Read token for public content (can be client-side for public data)
NEXT_PUBLIC_SANITY_API_TOKEN=skxxxxread
# Write token (server-side ONLY)
SANITY_API_WRITE_TOKEN=skxxxxwrite
Token Permissions
Create tokens with minimal permissions in the Sanity dashboard:
- Viewer - Read-only access to published content
- Editor - Read and write access (use server-side only)
- Deploy Studio - For CI/CD deployments
Token Exposure Risk: If your write token is exposed, attackers can modify or delete all your content. Always use read-only tokens for client-side code and keep write tokens in server-side environment variables only.
GROQ Query Safety
GROQ is Sanity's query language. Like SQL, it can be vulnerable to injection if you concatenate user input:
import { createClient } from '@sanity/client';
const client = createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,
apiVersion: '2024-01-01',
useCdn: true,
});
// DANGEROUS: String interpolation
async function searchPosts(searchTerm: string) {
// User could inject: `" || true || title == "`
const query = `*[_type == "post" && title match "${searchTerm}"]`;
return client.fetch(query);
}
// SAFE: Use parameters
async function searchPostsSafe(searchTerm: string) {
const query = `*[_type == "post" && title match $search]`;
return client.fetch(query, { search: `${searchTerm}*` });
}
// SAFE: Parameterized queries
async function getPostBySlug(slug: string) {
const query = `*[_type == "post" && slug.current == $slug][0]`;
return client.fetch(query, { slug });
}
// SAFE: Multiple parameters
async function getPostsByCategory(category: string, limit: number) {
const query = `*[_type == "post" && category == $category] | order(publishedAt desc)[0...$limit]`;
return client.fetch(query, { category, limit });
}
Content Access Control
// Fetching only published content
const query = `*[_type == "post" && !(_id in path("drafts.**"))]`;
// With authentication check for drafts
async function getPost(slug: string, preview: boolean) {
if (preview) {
// Requires authentication - use server-side only
const query = `*[_type == "post" && slug.current == $slug][0]`;
return previewClient.fetch(query, { slug });
}
// Public - only published content
const query = `*[_type == "post" && slug.current == $slug && !(_id in path("drafts.**"))][0]`;
return client.fetch(query, { slug });
}
CORS Configuration
Configure CORS in your Sanity dashboard to restrict API access:
// sanity.config.ts - Document your allowed origins
// Actual CORS is configured in Sanity dashboard
// Allowed origins should be:
// - https://yourdomain.com
// - https://www.yourdomain.com
// - http://localhost:3000 (development only)
// NEVER allow:
// - * (all origins)
// - http://*.yourdomain.com (wildcard subdomains)
Webhook Security
import crypto from 'crypto';
export async function POST(request: Request) {
const body = await request.text();
const signature = request.headers.get('sanity-webhook-signature');
if (!signature) {
return Response.json({ error: 'Missing signature' }, { status: 401 });
}
// Verify webhook signature
const expectedSignature = crypto
.createHmac('sha256', process.env.SANITY_WEBHOOK_SECRET!)
.update(body)
.digest('hex');
if (signature !== expectedSignature) {
return Response.json({ error: 'Invalid signature' }, { status: 401 });
}
// Safe to process webhook
const payload = JSON.parse(body);
// Revalidate cache, trigger builds, etc.
if (payload._type === 'post') {
await revalidatePath(`/blog/${payload.slug.current}`);
}
return Response.json({ success: true });
}
Preventing Data Leaks
// Be explicit about what fields you return
// RISKY: Returns all fields including internal ones
const query = `*[_type == "user"]`;
// SAFE: Explicit field selection
const query = `*[_type == "user"]{
_id,
name,
avatar,
bio
// Don't include: email, role, internalNotes
}`;
// For user-specific data, verify ownership
async function getUserProfile(userId: string, requesterId: string) {
// Only return full profile if user is viewing their own
if (userId !== requesterId) {
const query = `*[_type == "user" && _id == $userId][0]{
name,
avatar,
bio
}`;
return client.fetch(query, { userId });
}
// Full profile for own user
const query = `*[_type == "user" && _id == $userId][0]{
name,
email,
avatar,
bio,
settings
}`;
return client.fetch(query, { userId });
}
Sanity Security Checklist
- Write tokens stored server-side only
- Read-only tokens used for public content fetching
- GROQ queries use parameters, not string interpolation
- CORS configured with specific domains (no wildcards)
- Webhooks verify signature before processing
- Draft content requires authentication to view
- Queries explicitly select fields (no implicit *)
- Sensitive fields excluded from public queries
Can I use Sanity tokens on the client side?
Read-only tokens can be used client-side for public content. Never expose write tokens or tokens that can access draft/private content to the client.
Is GROQ vulnerable to injection like SQL?
Yes, if you concatenate user input into GROQ queries. Always use parameterized queries with the second argument to fetch(). Parameters are properly escaped.
How do I secure preview mode?
Preview mode should require authentication. Use a secret token in the preview URL and verify it server-side before showing draft content. Never expose draft content to unauthenticated users.
Scan Your Sanity Integration
Find GROQ injection vulnerabilities, exposed tokens, and access control issues before they reach production.
Start Free Scan