웹 앱이 브라우저에서 직접 OpenAI, Claude 또는 Gemini를 호출하면 API 키가 브라우저에 노출되며, DevTools를 여는 사람은 누구나 이를 복사할 수 있습니다. 2분이면 확인할 수 있습니다: Network 탭을 열고 AI 요청을 하나 보낸 다음, 제공자로 전송되는 Authorization, x-api-key 또는 x-goog-api-key 헤더를 찾아보세요. 거기서 키가 보인다면 오늘 바로 키를 교체하고 호출을 서버 라우트 뒤로 옮기세요.
아래에는 점검 방법, 수정 순서, 그리고 코딩 에이전트가 수정된 라우트를 테스트하는 방법이 있습니다.
브라우저에 노출된 API 키가 문제인 이유
브라우저에 노출된 API 키는 프런트엔드 코드나 네트워크 트래픽에 포함되어 전송되는 비밀 정보로, 모든 방문자가 읽을 수 있습니다. 브라우저가 키를 전송하면 사용자가 이를 복사해 귀하의 비용으로 제공자를 호출할 수 있습니다.
Google의 Gemini API 키 가이드는 “API 키를 웹 또는 모바일 앱에 직접 하드코딩하지 마세요”라고 말합니다. 왜냐하면 “클라이언트 측 코드에 컴파일된 키는 사용자가 추출할 수 있기” 때문이며, “실제 API 호출을 수행하려면 백엔드 프록시 서버를 실행하세요”라고 안내합니다. OpenAI의 API 키 안전 가이드도 같은 규칙을 제시합니다: 키를 클라이언트에 배포하는 대신 자체 백엔드를 통해 요청을 라우팅하세요.
openai-node README는 “비밀 API 자격 증명이 노출되는 것을 피하기 위해” 브라우저 지원이 기본적으로 비활성화되어 있으며, 이를 켜려면 dangerouslyAllowBrowser: true를 설정해야 한다고 말합니다. Anthropic의 TypeScript SDK도 같은 옵션 이름을 사용하며, 해당 API는 요청에 anthropic-dangerous-direct-browser-access: true 헤더가 있을 때만 브라우저(CORS) 호출을 허용합니다. 이는 출시 당시 Simon Willison이 문서화한 내용입니다.
이 패턴은 AI 앱 빌더와 코딩 에이전트로 빠르게 만든 앱에서 흔합니다. 가장 짧은 작동 경로는 React 컴포넌트에서 키를 VITE_ 또는 NEXT_PUBLIC_ 변수에 넣고 fetch하는 것입니다. 그러면 도난당한 키는 대량 추론을 위해 재판매됩니다. 도난당한 AI API 키와 LLMjacking에 관한 저희 글에서 다룬 것처럼요. 그리고 그 결과는 청구서에서 알게 됩니다.
API 키가 노출되었는지 확인하는 방법 (2분)

이 점검은 localhost가 아니라 프로덕션에 대해 실행하세요.
- Network 탭을 확인하세요(45초). 사이트를 열고 F12(Mac에서는 Cmd+Option+I)를 누른 뒤 Network 탭을 선택하고 채팅 메시지 전송 같은 AI 기능 하나를 실행하세요.
openai,anthropic또는googleapis로 필터링하세요. 요청이api.openai.com,api.anthropic.com또는generativelanguage.googleapis.com으로 직접 가면, 해당 요청을 클릭해 Headers를 여세요.Authorization: Bearer,x-api-key,x-goog-api-key또는?key=쿼리 매개변수에 키가 있으면 노출된 것입니다. - 번들을 검색하세요(45초). DevTools에서 Cmd+Option+F(Windows에서는 Ctrl+Shift+F)를 눌러 로드된 모든 스크립트를 검색하세요.
sk-,sk-ant-,AIza및dangerouslyAllowBrowser를 검색하세요. 브라우저에 제공되는.js파일에서 하나라도 발견되면 아직 요청이 발생하지 않았더라도 유출입니다. - 환경 변수 접두사를 확인하세요(30초).
.env파일과 호스팅 대시보드를 여세요. 빌드 도구는 설계상 일부 변수를 클라이언트 번들에 복사합니다: Vite의VITE_*(Vite 환경 변수 문서 참조), Next.js의NEXT_PUBLIC_*, Create React App의REACT_APP_*, Expo의EXPO_PUBLIC_*. 이러한 접두사 뒤에 있는 제공자 키는 이름이 무엇이든 공개됩니다.
빌드 출력을 터미널에서 스캔하려면:
# from your project root after a production build
grep -rnoE "sk-(proj-|ant-)?[A-Za-z0-9_-]{20,}|AIza[0-9A-Za-z_-]{35}" dist/ build/ .next/static/ 2>/dev/null
grep -rn "dangerouslyAllowBrowser" dist/ build/ .next/static/ 2>/dev/null
출력이 비어 있으면 통과입니다. Google은 2026년 5월 28일 이후 생성된 새로운 AI Studio 키는 “auth 키”이며 형식이 AIza와 다를 수 있다고 말하므로, 번들에서 generativelanguage.googleapis.com도 검색하세요. 브라우저가 자체 도메인(예: /api/chat)과 통신하고 제공자 호스트 이름이나 키가 어디에도 나타나지 않을 때만 통과입니다.
키가 노출되었을 때 해야 할 일: 수정 순서
먼저 키를 교체하면 오늘의 구멍이 막히고, 아키텍처 변경은 이를 영구히 막습니다.
| 단계 | 수행할 작업 | 이 순서인 이유 | 시간 |
|---|---|---|---|
| 1. 키 교체 | 제공자 콘솔에서 새 키를 만들고 유출된 키를 폐기하세요 | 이전 키는 이미 복사되었을 수 있으며, 폐기만이 사용을 막는 유일한 방법입니다 | 5분 |
| 2. 호출을 서버 측으로 이동 | 키를 보관하는 서버 라우트 또는 엣지 함수에 제공자 호출을 넣으세요 | 새 키가 같은 방식으로 유출되는 것을 막습니다 | 30~60분 |
| 3. 인증 및 사용자별 속도 제한 추가 | 라우트에 세션을 요구하고 사용자당 요청 수를 제한하세요 | 이것이 없으면 프록시가 누구나 쓰는 무료 AI 엔드포인트가 됩니다 | 30분 |
| 4. 제공자 지출 한도 설정 | 각 제공자의 결제 설정에서 예산 또는 지출 한도를 설정하세요 | 다른 것이 유출될 경우 최악의 청구서를 제한합니다 | 10분 |
| 5. CI에서 스캔 | 출력 번들에 키 패턴이 나타나면 빌드를 실패시키세요 | 다음 에이전트가 작성한 기능이 수정을 되돌리는 것을 방지합니다 | 15분 |
이전 키를 폐기하기 전에 새 키를 서버 측에 배포하세요. 그렇지 않으면 AI 기능이 중단됩니다. API 키 교체 모범 사례에 관한 저희 가이드에서는 무중단 교체를 다룹니다. 4단계에서 OpenAI의 키 안전 지침은 강제 적용되는 지출 한도를 권장합니다. 경고만으로는 트래픽을 막지 못하기 때문입니다. Google Cloud 결제 예산은 경고를 보내지만 그 자체로 사용을 중단시키지는 않습니다.
OpenAI, Claude 및 Gemini 호출을 서버 라우트 뒤로 옮기는 방법

백엔드 프록시 라우트는 사용자의 프롬프트를 받고, 서버에서 API 키를 추가하고, 제공자를 호출한 뒤 답변만 반환하는 작은 서버 엔드포인트입니다. 브라우저는 키를 절대 보지 못합니다. 다음은 OpenAI를 위한 최소한의 Next.js 라우트입니다. Claude와 Gemini도 같은 형태를 따릅니다:
// app/api/chat/route.ts (runs on the server only)
import OpenAI from "openai";
import { getSession } from "@/lib/auth"; // your auth helper
import { checkRateLimit } from "@/lib/limits"; // your per-user limiter
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); // no NEXT_PUBLIC_ prefix
export async function POST(req: Request) {
const session = await getSession(req);
if (!session) return new Response("Unauthorized", { status: 401 });
if (!(await checkRateLimit(session.userId, 20))) { // 20 requests per minute
return new Response("Too many requests", { status: 429 });
}
const { message } = await req.json();
if (typeof message !== "string" || message.length > 4000) {
return new Response("Bad request", { status: 400 });
}
const response = await client.responses.create({
model: process.env.OPENAI_MODEL!, // pin the model server-side
input: message,
});
return Response.json({ reply: response.output_text }); // return the answer, nothing else
}
안전하게 만드는 요소는 단순히 숨기는 것이 아닙니다:
- 모델은 서버에 있습니다. 브라우저가 모델을 고르면 공격자는 가장 비싼 모델을 고릅니다.
- 입력은 제한됩니다. 귀하의 계정으로 거대한 프롬프트가 전달되지 않습니다.
- 답변만 반환됩니다. 제공자의 원시 응답이나 오류 객체를 절대 반환하지 마세요.
- 공개 접두사가 없습니다.
OPENAI_API_KEY는 서버 측에 남아 있고,NEXT_PUBLIC_OPENAI_API_KEY는 모든 방문자에게 전송됩니다.
Claude와 Gemini의 경우 ANTHROPIC_API_KEY 또는 GEMINI_API_KEY와 함께 해당 SDK를 사용하세요. Google의 웹사이트 및 IP 제한은 도움이 되지만, curl을 가진 사람은 누구나 임의의 Referer 헤더를 보낼 수 있으므로 이를 두 번째 방어 계층으로 취급하세요. 브라우저 호출이 정당한 유일한 경우는 각 사용자가 자신의 키를 붙여 넣는 bring-your-own-key 도구입니다.
코딩 에이전트가 Apidog CLI로 프록시 라우트를 테스트하게 하기
키를 옮기는 것은 절반의 작업입니다. 나머지 절반은 에이전트가 다음 기능을 작성한 후에도 키가 옮겨진 상태로 유지됨을 증명하는 것입니다. 라우트를 만든 에이전트가 Apidog CLI로 테스트하도록 하세요. 한 번 설정하세요:
npm install -g apidog-cli@latest
apidog login --with-token <YOUR_TOKEN>
npx -y skills add https://apidog.com # pick your agent when prompted
그런 다음 Claude Code, Codex, Cursor 또는 사용 중인 에이전트에 다음 프롬프트를 주세요:
Create Apidog test cases in project <id> for POST /api/chat on our staging
environment that assert:
1. a request without a session cookie returns 401,
2. a valid request returns 200 and the body does not contain "sk-", "sk-ant-",
"AIza" or "api_key",
3. the 21st request inside 60 seconds from the same user returns 429.
Run them and summarize any failures.
에이전트는 스키마를 읽고, 페이로드를 검증하고, 케이스를 작성하고 실행합니다:
apidog test-case category --project <id>
apidog cli-schema get test-case-create
apidog cli-schema validate test-case-create --file case.json
apidog test-case create --project <id> --file case.json
apidog run --test-case <caseId> --project <id> -e <envId> -r cli,json
두 번째 검증이 가장 중요합니다. 제공자 오류 객체나 디버그 필드가 키를 클라이언트로 다시 에코하기 시작하는 순간 실패하기 때문입니다. 동일한 apidog run ... -r junit 단계를 파이프라인에 넣어 모든 배포가 이를 다시 확인하게 하세요. Apidog CLI CI/CD 가이드에서 설정을 보여주고, Claude Code에서의 Apidog CLI는 에이전트 측을 다룹니다. Apidog의 무료 플랜은 최대 4명의 사용자를 지원하므로 소규모 팀은 유료 좌석 없이 이를 실행할 수 있습니다.
같은 문제의 저장소 측면, 즉 브라우저로 전송되는 대신 Git에 커밋된 키에 대해서는 시크릿 스캐너를 사용한 API 키 유출 확인을 참고하세요. 에이전트가 고객 데이터를 처리하는 도구를 호출한다면, AI 에이전트의 데이터 유출 테스트가 다음 계층을 다룹니다.
키를 계속 노출되게 만드는 흔한 실수
- 키를 Base64로 인코딩하거나 분할하기. 브라우저는 전송하려면 키를 재구성해야 하므로 DevTools에 실제 값이 표시됩니다.
- localStorage나 쿠키로 옮기기. 여전히 클라이언트에 있습니다.
- 인증 없이 프록시하기. 공개된
/api/chat은 귀하의 비용으로 열린 AI 엔드포인트입니다. - “혹시 모르니” 이전 키를 남겨두기. 하루 동안 공개된 키는 복사됩니다.
- 모바일 앱을 잊기. Google 가이드가 “웹 또는 모바일 앱”을 함께 언급하는 데는 이유가 있습니다.
자주 묻는 질문
브라우저에서 내 OpenAI API 키가 노출되었는지 어떻게 알 수 있나요? DevTools Network 탭을 연 상태에서 AI 기능을 실행하세요. Authorization: Bearer sk-... 헤더와 함께 api.openai.com으로 요청이 가면 노출된 것입니다. 또한 로드된 스크립트에서 sk-와 dangerouslyAllowBrowser를 검색하세요.
API 키를 NEXT_PUBLIC_ 또는 VITE_ 환경 변수에 넣어도 안전한가요? 아닙니다. 해당 접두사는 값을 클라이언트 번들에 복사하기 위해 존재합니다. 그 아래에 저장된 제공자 키는 모든 방문자에게 전송됩니다. 접두사 없는 서버 전용 변수를 사용하고 서버 라우트 내부에서 읽으세요.
Gemini API 키를 내 웹사이트로 제한하면 브라우저에서 안전해지나요? 도움이 되지만, 브라우저 외부의 스크립트는 임의의 Referer 헤더를 보낼 수 있습니다. Google의 자체 가이드도 클라이언트 앱에는 백엔드 프록시를 권장합니다.
키가 노출된 것을 발견했습니다. 먼저 무엇을 해야 하나요? 새 키를 만들고 서버 측에 배포한 다음 이전 키를 폐기하세요. 그다음 호출을 사용자별 속도 제한이 있는 인증된 라우트 뒤로 옮기고, 제공자 지출 한도를 설정하세요.
코딩 에이전트가 이 문제를 수정하고 테스트할 수 있나요? 예. 제공자 호출을 서버 라우트로 옮기도록 요청한 다음, Apidog CLI를 사용해 세션 없이 401, 응답에 키 패턴 없음, 속도 제한 초과 시 429를 검증하는 테스트를 만들고 실행하세요.
