[{"data":1,"prerenderedAt":255},["ShallowReactive",2],{"blog-guides/sanity":3},{"id":4,"title":5,"body":6,"category":233,"date":234,"dateModified":235,"description":236,"draft":237,"extension":238,"faq":239,"featured":237,"headerVariant":240,"image":239,"keywords":241,"meta":242,"navigation":243,"ogDescription":244,"ogTitle":239,"path":245,"readTime":246,"schemaOrg":247,"schemaType":248,"seo":249,"sitemap":250,"stem":251,"tags":252,"twitterCard":253,"__hash__":254},"blog/blog/guides/sanity.md","Sanity CMS Security Guide for Vibe Coders",{"type":7,"value":8,"toc":219},"minimark",[9,16,21,24,28,39,44,47,70,79,83,86,92,96,102,106,109,115,119,125,129,135,140,166,188,207],[10,11,12],"tldr",{},[13,14,15],"p",{},"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.",[17,18,20],"h2",{"id":19},"why-sanity-security-matters-for-vibe-coding","Why Sanity Security Matters for Vibe Coding",[13,22,23],{},"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.",[17,25,27],{"id":26},"api-token-management","API Token Management",[29,30,35],"pre",{"className":31,"code":33,"language":34},[32],"language-text","# .env.local (never commit)\n# Project configuration (safe for client)\nNEXT_PUBLIC_SANITY_PROJECT_ID=your-project-id\nNEXT_PUBLIC_SANITY_DATASET=production\n\n# Read token for public content (can be client-side for public data)\nNEXT_PUBLIC_SANITY_API_TOKEN=skxxxxread\n\n# Write token (server-side ONLY)\nSANITY_API_WRITE_TOKEN=skxxxxwrite\n","text",[36,37,33],"code",{"__ignoreMap":38},"",[40,41,43],"h3",{"id":42},"token-permissions","Token Permissions",[13,45,46],{},"Create tokens with minimal permissions in the Sanity dashboard:",[48,49,50,58,64],"ul",{},[51,52,53,57],"li",{},[54,55,56],"strong",{},"Viewer"," - Read-only access to published content",[51,59,60,63],{},[54,61,62],{},"Editor"," - Read and write access (use server-side only)",[51,65,66,69],{},[54,67,68],{},"Deploy Studio"," - For CI/CD deployments",[71,72,73],"warning-box",{},[13,74,75,78],{},[54,76,77],{},"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.",[17,80,82],{"id":81},"groq-query-safety","GROQ Query Safety",[13,84,85],{},"GROQ is Sanity's query language. Like SQL, it can be vulnerable to injection if you concatenate user input:",[29,87,90],{"className":88,"code":89,"language":34},[32],"import { createClient } from '@sanity/client';\n\nconst client = createClient({\n  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,\n  dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,\n  apiVersion: '2024-01-01',\n  useCdn: true,\n});\n\n// DANGEROUS: String interpolation\nasync function searchPosts(searchTerm: string) {\n  // User could inject: `\" || true || title == \"`\n  const query = `*[_type == \"post\" && title match \"${searchTerm}\"]`;\n  return client.fetch(query);\n}\n\n// SAFE: Use parameters\nasync function searchPostsSafe(searchTerm: string) {\n  const query = `*[_type == \"post\" && title match $search]`;\n  return client.fetch(query, { search: `${searchTerm}*` });\n}\n\n// SAFE: Parameterized queries\nasync function getPostBySlug(slug: string) {\n  const query = `*[_type == \"post\" && slug.current == $slug][0]`;\n  return client.fetch(query, { slug });\n}\n\n// SAFE: Multiple parameters\nasync function getPostsByCategory(category: string, limit: number) {\n  const query = `*[_type == \"post\" && category == $category] | order(publishedAt desc)[0...$limit]`;\n  return client.fetch(query, { category, limit });\n}\n",[36,91,89],{"__ignoreMap":38},[17,93,95],{"id":94},"content-access-control","Content Access Control",[29,97,100],{"className":98,"code":99,"language":34},[32],"// Fetching only published content\nconst query = `*[_type == \"post\" && !(_id in path(\"drafts.**\"))]`;\n\n// With authentication check for drafts\nasync function getPost(slug: string, preview: boolean) {\n  if (preview) {\n    // Requires authentication - use server-side only\n    const query = `*[_type == \"post\" && slug.current == $slug][0]`;\n    return previewClient.fetch(query, { slug });\n  }\n\n  // Public - only published content\n  const query = `*[_type == \"post\" && slug.current == $slug && !(_id in path(\"drafts.**\"))][0]`;\n  return client.fetch(query, { slug });\n}\n",[36,101,99],{"__ignoreMap":38},[17,103,105],{"id":104},"cors-configuration","CORS Configuration",[13,107,108],{},"Configure CORS in your Sanity dashboard to restrict API access:",[29,110,113],{"className":111,"code":112,"language":34},[32],"// sanity.config.ts - Document your allowed origins\n// Actual CORS is configured in Sanity dashboard\n\n// Allowed origins should be:\n// - https://yourdomain.com\n// - https://www.yourdomain.com\n// - http://localhost:3000 (development only)\n\n// NEVER allow:\n// - * (all origins)\n// - http://*.yourdomain.com (wildcard subdomains)\n",[36,114,112],{"__ignoreMap":38},[17,116,118],{"id":117},"webhook-security","Webhook Security",[29,120,123],{"className":121,"code":122,"language":34},[32],"import crypto from 'crypto';\n\nexport async function POST(request: Request) {\n  const body = await request.text();\n  const signature = request.headers.get('sanity-webhook-signature');\n\n  if (!signature) {\n    return Response.json({ error: 'Missing signature' }, { status: 401 });\n  }\n\n  // Verify webhook signature\n  const expectedSignature = crypto\n    .createHmac('sha256', process.env.SANITY_WEBHOOK_SECRET!)\n    .update(body)\n    .digest('hex');\n\n  if (signature !== expectedSignature) {\n    return Response.json({ error: 'Invalid signature' }, { status: 401 });\n  }\n\n  // Safe to process webhook\n  const payload = JSON.parse(body);\n\n  // Revalidate cache, trigger builds, etc.\n  if (payload._type === 'post') {\n    await revalidatePath(`/blog/${payload.slug.current}`);\n  }\n\n  return Response.json({ success: true });\n}\n",[36,124,122],{"__ignoreMap":38},[17,126,128],{"id":127},"preventing-data-leaks","Preventing Data Leaks",[29,130,133],{"className":131,"code":132,"language":34},[32],"// Be explicit about what fields you return\n// RISKY: Returns all fields including internal ones\nconst query = `*[_type == \"user\"]`;\n\n// SAFE: Explicit field selection\nconst query = `*[_type == \"user\"]{\n  _id,\n  name,\n  avatar,\n  bio\n  // Don't include: email, role, internalNotes\n}`;\n\n// For user-specific data, verify ownership\nasync function getUserProfile(userId: string, requesterId: string) {\n  // Only return full profile if user is viewing their own\n  if (userId !== requesterId) {\n    const query = `*[_type == \"user\" && _id == $userId][0]{\n      name,\n      avatar,\n      bio\n    }`;\n    return client.fetch(query, { userId });\n  }\n\n  // Full profile for own user\n  const query = `*[_type == \"user\" && _id == $userId][0]{\n    name,\n    email,\n    avatar,\n    bio,\n    settings\n  }`;\n  return client.fetch(query, { userId });\n}\n",[36,134,132],{"__ignoreMap":38},[136,137,139],"h4",{"id":138},"sanity-security-checklist","Sanity Security Checklist",[48,141,142,145,148,151,154,157,160,163],{},[51,143,144],{},"Write tokens stored server-side only",[51,146,147],{},"Read-only tokens used for public content fetching",[51,149,150],{},"GROQ queries use parameters, not string interpolation",[51,152,153],{},"CORS configured with specific domains (no wildcards)",[51,155,156],{},"Webhooks verify signature before processing",[51,158,159],{},"Draft content requires authentication to view",[51,161,162],{},"Queries explicitly select fields (no implicit *)",[51,164,165],{},"Sensitive fields excluded from public queries",[167,168,169,176,182],"faq-section",{},[170,171,173],"faq-item",{"question":172},"Can I use Sanity tokens on the client side?",[13,174,175],{},"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.",[170,177,179],{"question":178},"Is GROQ vulnerable to injection like SQL?",[13,180,181],{},"Yes, if you concatenate user input into GROQ queries. Always use parameterized queries with the second argument to fetch(). Parameters are properly escaped.",[170,183,185],{"question":184},"How do I secure preview mode?",[13,186,187],{},"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.",[189,190,191,197,202],"related-articles",{},[192,193],"related-card",{"description":194,"href":195,"title":196},"Row-level security and auth patterns","/blog/guides/supabase","Supabase Security Guide",[192,198],{"description":199,"href":200,"title":201},"Security rules and authentication","/blog/guides/firebase","Firebase Security Guide",[192,203],{"description":204,"href":205,"title":206},"Best practices for key management","/blog/how-to/secure-api-keys","Secure API Keys",[208,209,212,216],"cta-box",{"href":210,"label":211},"/","Start Free Scan",[17,213,215],{"id":214},"scan-your-sanity-integration","Scan Your Sanity Integration",[13,217,218],{},"Find GROQ injection vulnerabilities, exposed tokens, and access control issues before they reach production.",{"title":38,"searchDepth":220,"depth":220,"links":221},2,[222,223,227,228,229,230,231,232],{"id":19,"depth":220,"text":20},{"id":26,"depth":220,"text":27,"children":224},[225],{"id":42,"depth":226,"text":43},3,{"id":81,"depth":220,"text":82},{"id":94,"depth":220,"text":95},{"id":104,"depth":220,"text":105},{"id":117,"depth":220,"text":118},{"id":127,"depth":220,"text":128},{"id":214,"depth":220,"text":215},"guides","2026-01-28","2026-02-16","Secure your Sanity CMS when vibe coding. Learn API token management, GROQ injection prevention, webhook security, and content access control patterns.",false,"md",null,"blue","Sanity security, headless CMS security, vibe coding CMS, GROQ security, content API security",{},true,"Secure your Sanity CMS with proper API token management and GROQ query safety.","/blog/guides/sanity","10 min read","[object Object]","TechArticle",{"title":5,"description":236},{"loc":245},"blog/guides/sanity",[],"summary_large_image","QorQrvk68oqv6LGFT7absjui0r9QcKpahYIs2grQBRM",1775843929642]