v0 샌드박스에서 생성된 코드는 Snowflake에 쿼리를 보내지만, OAuth 토큰은 샌드박스 안으로 절대 들어가지 않습니다. 프로토콜 인식 프록시가 요청 시점에 자격 증명을 주입하는 방식을 사용하는데, 단순히 토큰을 텍스트로 치환하는 방식 자체가 오히려 정보 유출의 원인이 될 수 있었다는 점도 함께 짚어줍니다.
AI가 생성한 애플리케이션은 사용자를 대신해 외부 서비스에 인증해야 하는 경우가 많습니다. 여기서 문제가 생깁니다. 생성된 코드가 사용자의 자격 증명에 접근해서는 안 된다는 것입니다.
v0의 Snowflake 통합을 구축할 때 바로 이 문제에 직면했습니다. 이 통합 기능을 통해 사용자는 Snowflake를 연결하고, 스키마를 살펴보고, 데이터를 쿼리하고, 자신의 웨어하우스를 대상으로 실행되는 애플리케이션을 생성할 수 있습니다. 생성된 코드는 Snowflake에 인증해야 하지만, 모델이 작성하고 사람의 검토 없이 실행되는 코드인 만큼 프롬프트 인젝션을 통해 읽을 수 있는 데이터를 외부로 유출하도록 유도될 수 있습니다. 그래서 사용자의 OAuth 토큰은 코드가 실행되는 환경 안으로 절대 들어가서는 안 됩니다.
이 문제는 Vercel 샌드박스 방화벽 위에 구축한 v0 샌드박스용 Snowflake 요청 프록시로 해결했습니다. 샌드박스 안에서는 일반 Snowflake 클라이언트를 그대로 실행할 수 있지만, 실제 자격 증명은 샌드박스 런타임 외부의 서버 프록시에서 요청 시점에 처리됩니다. 덕분에 기존 Snowflake 클라이언트를 샌드박스 안에서 그대로 사용하면서도 생성된 코드에 사용자 자격 증명이 노출되지 않습니다.
더 까다로운 문제는 프록시가 자격 증명을 안전하게 주입할 위치를 결정하는 것이었습니다. 가장 직관적인 구현 방식, 즉 플레이스홀더 토큰이 나타나는 모든 곳을 실제 토큰으로 치환하는 방식은 오히려 또 다른 자격 증명 유출을 초래합니다.
v0는 생성된 애플리케이션을 격리된 샌드박스에서 실행합니다. 격리는 신뢰할 수 없는 코드로부터 나머지 시스템을 보호하지만, 샌드박스 내부의 비밀을 지켜주지는 않습니다.
생성된 앱이 파일 시스템에서 토큰을 읽을 수 있다면, 그 토큰은 로그에 기록되거나, API 응답으로 반환되거나, 생성된 클라이언트 코드에 포함되거나, 다른 호스트로 전송될 수 있습니다. 샌드박스 격리는 애플리케이션이 접근할 수 있는 범위를 제한하지만, 자격 증명 자체가 이미 샌드박스 안에 들어와 있다면 아무 소용이 없습니다.
Snowflake의 경우, 자격 증명은 사용자가 연결한 Snowflake 역할을 나타냅니다. v0는 사용자가 접근 권한을 가진 데이터를 탐색하고 활용할 수 있도록 도와야 하지만, 생성된 코드가 단지 자격 증명이 필요하다는 이유만으로 공급자의 원시 자격 증명을 그대로 받아서는 안 됩니다.
샌드박스는 Snowflake와 직접 통신할 수 없습니다. 샌드박스 내부의 코드가 사용자의 Snowflake 계정 호스트로 요청을 보내면, 샌드박스 방화벽이 해당 요청을 v0 Snowflake 프록시로 전달합니다.
방화벽은 각 샌드박스마다 고유한 인증 기관(CA)으로 TLS를 종료합니다. 이를 통해 프록시는 암호화되었을 트래픽을 읽고 재작성할 수 있습니다. 샌드박스는 해당 CA를 자동으로 신뢰하므로, Snowflake SDK와 CLI는 OCSP를 포함한 기본 인증서 유효성 검사를 그대로 사용합니다. 이후 프록시는 샌드박스의 OIDC 토큰을 검증하고, 해당 샌드박스가 속한 v0 채팅을 조회한 뒤, 그 채팅에 바인딩된 사용자 세션을 복원하고 해당 사용자의 새로운 Snowflake 자격 증명을 가져옵니다.
토큰이 포함된 요청의 전송 목적지는 샌드박스가 결정하지 않습니다. 프록시는 서버 측 자격 증명에서 Snowflake 계정 호스트를 도출하고, 유효하지 않은 계정 URL은 거부합니다. 생성된 코드가 제공하는 호스트 정보를 신뢰하는 대신, 자격 증명이 연결된 Snowflake 계정 범위 안에서만 사용되도록 제한하는 것입니다.
임시방편으로, 통합의 초기 버전에서는 사용자의 실제 토큰을 샌드박스의 Snowflake 토큰 파일에 직접 기록했습니다. 프록시 방식은 이 접근법을 대체합니다. 이상적으로는 샌드박스에서 토큰 파일을 완전히 제거하는 것이 좋겠지만, Snowflake 클라이언트는 인증 흐름에 따라 서로 다른 위치에서 자격 증명을 찾기 때문에 그렇게 할 수 없습니다.
Snowflake SQL API 호출과 같은 일부 요청은 Authorization: Bearer 헤더로 인증합니다. 다른 클라이언트 흐름은 로컬 토큰 파일을 읽어 로그인 요청의 일부로 토큰을 전송합니다. 클라이언트 인증이 완료되면, 이후 요청은 Snowflake가 발급한 세션 토큰을 사용하며, 프록시는 이 요청들을 그대로 통과시킵니다.
호환성을 유지하기 위해, v0는 토큰 형태의 플레이스홀더를 샌드박스에 계속 기록합니다. 이 플레이스홀더는 어떠한 접근 권한도 부여하지 않는 고정된 72바이트 공개 문자열로, 기존 Snowflake SDK 및 CLI 흐름이 토큰이 존재하는 것처럼 동작하게 하는 것이 유일한 역할입니다. 프록시는 플레이스홀더 자체를 기반으로 요청을 인증하지 않습니다. 대신, 샌드박스의 서버 측 ID와 사용자 채팅에 대한 바인딩을 사용하여 요청을 인증합니다.
사용자를 나타내는 재사용 가능한 자격 증명인 실제 OAuth 토큰은 샌드박스에 절대 기록되지 않습니다. Snowflake는 로그인 후 샌드박스 내부에 세션 토큰을 발급하지만, 각 토큰은 하나의 인증된 세션에만 속합니다.
실제로 이 세션들은 단명합니다. 생성된 앱의 Snowflake 헬퍼는 각 쿼리 후 연결을 종료하므로 세션이 즉시 끝납니다. 채팅을 닫는다고 세션이 바로 종료되지는 않지만, 샌드박스를 해제하면 토큰도 함께 사라지며, Snowflake는 기본적으로 비활성 상태가 4시간 지속되면 서버 측에서 세션을 만료시킵니다. 프록시는 요청이 도달하면 자격 증명을 첨부할지, 어디에 첨부할지를 결정합니다.
초기 버전의 프록시는 각 요청에서 플레이스홀더를 검색해 나타나는 모든 곳에 실제 토큰으로 치환했습니다.
문제는 생성된 코드가 요청의 일부, 즉 플레이스홀더가 나타날 수 있는 위치를 제어한다는 점입니다. 예를 들어 SQL 문은 호출자가 제어하는 데이터입니다. 쿼리에 플레이스홀더가 문자열 리터럴로 포함되어 있으면, 단순 치환으로 인해 실제 OAuth 토큰을 담은 쿼리가 만들어집니다. 그리고 데이터베이스가 해당 문자열을 반환하면 실제 토큰이 쿼리 출력으로 샌드박스 안에 다시 들어오게 됩니다. 프록시가 막으려 했던 바로 그 토큰을 스스로 유출하는 셈입니다.
더 엄격한 매칭 방식으로도 이 취약점은 해결되지 않습니다. 프록시는 공격자가 제어하는 텍스트를 기반으로 자격 증명을 주입해서는 안 됩니다. 그 대신, 각 Snowflake 요청에서 인증 정보를 담는 필드가 어느 것인지 정확히 파악해야 합니다.
Snowflake 요청은 세 가지 형태로 프록시에 도달하며, 각 형태마다 인증 정보가 위치하는 곳이 다릅니다.
Snowflake SQL API 요청의 경우, 프록시는 Authorization: Bearer 헤더에 OAuth 토큰을 설정합니다. 본문은 호출자가 제어하는 SQL 그대로 유지되며 재작성하지 않습니다. SQL 페이로드에 플레이스홀더가 나타나면, 프록시는 요청이 Snowflake에 도달하기 전에 거부하고 플레이스홀더 오용으로 기록합니다.
Snowflake 로그인 요청의 경우, 프록시는 JSON 요청 본문을 파싱하고, 로그인 토큰 필드에만 구조적으로 토큰을 설정한 뒤 본문을 다시 직렬화합니다. 요청 본문의 다른 곳에 플레이스홀더가 남아 있으면, 프록시는 안전 차단(fail closed) 처리합니다.
로그인 후의 세션 요청은 Snowflake가 관리하는 세션 토큰으로 인증하므로, 프록시가 주입할 내용이 없습니다.
프록시는 다음 중 하나라도 해당하는 요청을 거부합니다.
샌드박스가 채팅에 바인딩되어 있지 않은 경우
사용자 범위의 자격 증명을 가져올 수 없는 경우
Snowflake 계정 호스트를 도출할 수 없는 경우
플레이스홀더가 승인된 인증 필드 외부에 나타나는 경우
구조화된 로그인 본문을 안전하게 파싱할 수 없는 경우
요청은 검사 전에 크기 제한이 적용되므로, 압축되었거나 지나치게 큰 본문이 프록시를 무한 파서로 만들 수 없습니다. 프록시를 통과하는 모든 요청은 업스트림 결과, 상태, 처리 시간, 주입 위치를 담은 옵저버빌리티(observability) 이벤트를 생성합니다. 이를 통해 비밀을 노출하지 않고도 오용과 통합 오류를 파악할 수 있습니다.
토큰 갱신은 주변 브라우저 쿠키에 의존할 수 없습니다. 프록시 요청이 사용자의 브라우저 세션이 아닌 샌드박스에서 발생하기 때문입니다. 대신 v0는 사용자 세션을 샌드박스에 바인딩하고, 프록시는 해당 바인딩을 통해 OAuth 자격 증명을 발급하고 갱신합니다.
게시 경로는 동일한 자격 증명 경계 아래 Snowflake CLI 흐름을 사용합니다. 배포 시 자격 증명 파일에는 플레이스홀더만 기록되며, 실제 토큰은 절대 기록되지 않습니다.
배포 후 애플리케이션은 Snowpark Container Services에서 실행되며, Snowflake가 자동으로 관리하고 교체하는 토큰을 사용해 자체 서비스 사용자로 인증합니다. 이 토큰은 /snowflake/session/token에 마운트됩니다. 이 시점부터 사용자의 OAuth 토큰과 v0 프록시는 더 이상 관여하지 않습니다.
사용자 입장에서 이 모든 과정은 보이지 않습니다. Snowflake를 연결하고, v0에게 스키마를 살펴보거나 앱을 만들어 달라고 요청하고, 결과를 미리 보고, 준비가 되면 배포하면 됩니다.
이 보안 모델은 다섯 가지 원칙 위에 세워져 있습니다.
자격 증명 주입은 각 엔드포인트의 인증 필드에서만 이루어진다
인증 필드 외부에서 플레이스홀더를 사용하는 요청은 거부되고 기록된다
토큰이 포함된 요청은 연결된 Snowflake 계정 호스트로만 전송된다
자격 증명은 서버 측에서 발급되고 갱신된다
생성된 코드는 사용자의 OAuth 토큰을 읽지 않고도 Snowflake를 사용한다
프로덕션 배포 후 첫 15일 동안, 프록시는 약 13,000건의 요청에 대해 서버 측에서 자격 증명을 첨부했으며, 플레이스홀더 오용으로 인한 거부는 단 한 건도 기록되지 않았습니다.
이 프록시는 Snowflake를 위해 구축했지만, 동일한 문제는 다른 통합에도 적용됩니다. 생성된 코드는 사용자의 장기 자격 증명을 받지 않고도 인증해야 하는 경우가 많습니다. 핵심은 임의의 요청 데이터를 재작성하는 대신, 프로토콜에서 정의한 인증 필드에만 자격 증명을 주입하는 것입니다.
v0 Snowflake 통합은 현재 베타로 제공됩니다. 통합 문서를 통해 시작해 보세요.