본문으로 건너뛰기
NEW · Blast Radius: 유출된 API 키 대응 플레이북 (AWS·GCP·Azure)
블로그로 돌아가기

프런트엔드 API 키 노출: 공개 키와 비밀 키 구분하기

브라우저에 보이는 키가 공개용인지 비밀 키인지 발급처에서 확인하고, Resend 사고와 빌드 결과를 근거로 대응합니다.

편집 기록: 이전 글의 사이트별 노출률은 원자료를 확인할 수 없어 제거했습니다. Resend 사례는 회사의 사고 보고서에 근거합니다.

김동현
작성자
김동현
발행
수정
6 분622 어절
Share:
Stripe 게시 가능 키 pk_live_와 비밀 키 sk_live_를 구분한 설명용 도식

2026년 10월 4일 수정. 이전 글은 사이트별 자격증명 노출률을 Cremit의 조사 결과로 제시했지만, 이를 뒷받침할 사이트 표본, 결과 기록, 검증 절차를 찾지 못했습니다. 해당 비율과 그에 기반한 사례를 제거했습니다. 아래 Resend 사고는 회사가 공개한 보고서와 그 한계를 명시해 남겼습니다.

프런트엔드 JavaScript에 API 키가 있으면 유출인가요?

공개 JavaScript 번들을 받은 사람은 그 안의 값을 읽을 수 있습니다. 다만 보이는 값이 비밀 키인지는 발급처가 정한 키의 종류와 용도에 따라 다릅니다. Stripe의 pk_live_ 게시 가능 키는 클라이언트 코드용입니다. sk_live_ 비밀 키와 rk_live_ 제한 키는 그렇지 않습니다. 값이 어디에 있는지 확인한 뒤 발급처에서 종류와 현재 상태를 확인합니다. 접두어 일치만으로 악용을 입증할 수는 없습니다.

Resend: 공개 사고 보고서로 확인되는 클라이언트 키 노출

Resend의 2024년 1월 10일 사고 보고서에 따르면 공격자는 2023년 12월 30일 Resend 대시보드 클라이언트 쪽에 환경 변수로 노출된 데이터베이스 API 키를 발견했습니다. 1월 7일부터 이 접근으로 고객 데이터를 읽었습니다. Resend는 1월 10일 클라이언트의 해당 변수를 제거하고 데이터베이스 키를 교체했습니다. 보고서는 이메일 본문, 암호화되지 않은 Resend API 개인 키, 암호화되지 않은 DKIM 개인 키는 접근되지 않았다고 밝힙니다. 이 사례는 서버용 데이터베이스 API 자격증명이 클라이언트 경로로 나온 것이지, 공개 프런트엔드 키라면 모두 데이터베이스에 접근할 수 있다는 뜻은 아닙니다.

핵심은 경계입니다. 서버에서만 써야 할 값이 브라우저 쪽에 도달했고 발급처가 그 값을 받아들였습니다. 조사에는 전달된 클라이언트 코드와 데이터베이스 API 접근 로그가 모두 필요했습니다. 소스 코드에서 패턴을 찾는 것만으로 접근 시각이나 영향을 받은 자료를 알 수는 없습니다.

내 애플리케이션에서 볼 세 곳

1. 클라이언트 코드와 빌드 산출물

프레임워크의 공개 변수 접두어, 클라이언트 컴포넌트에 들어간 값, 서버 컴포넌트에서 HTML이나 클라이언트 속성으로 넘긴 값을 확인합니다. Next.js는 직접 참조한 NEXT_PUBLIC_ 변수를 빌드할 때 클라이언트 번들에 넣습니다. next.config.js env 값도 번들에 포함됩니다. 호스팅 대시보드에 안전하게 저장한 키라도 애플리케이션 코드가 이 경로로 내보내면 노출됩니다.

typescript
// 공개 설정값: 브라우저 코드에서 사용 가능
const stripePublishableKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY;

// 비밀 키라면 잘못된 클라이언트 경계
const stripeSecretKey = process.env.NEXT_PUBLIC_STRIPE_SECRET_KEY;

2. 공개 응답과 정적 파일

직접 관리하는 운영·프리뷰 배포가 제공하는 HTML, JSON 응답, 공개된 소스맵, 정적 파일을 살핍니다. JavaScript 변수 이름이 없어도 값이 밖으로 나올 수 있습니다. 코드 압축은 읽기 어렵게 할 뿐 접근을 막지 않습니다. 티켓에는 키 전체를 복사하지 말고 URL과 빌드 식별자만 기록합니다.

3. 저장소와 빌드 기록

.env 파일, 설정 파일, 키가 든 빌드 산출물이 Git에 커밋됐는지 확인합니다. 이 경로는 브라우저 배포와 별개입니다. 클라이언트 번들이 깨끗해도 Git 기록이나 CI 로그에 키가 없었다는 뜻은 아닙니다. 발견 위치와 그곳을 읽을 수 있던 대상을 기록합니다.

로컬에서 안전하게 시작하는 검사

자신의 프로젝트에서 새로 만든 빌드에 알려진 비밀 키 접두어가 있는지 찾습니다. 아래 명령은 일치한 값 대신 파일 이름만 출력합니다. 지정한 접두어와 경로만 검사하므로 발급처별 스캐너, HTML·응답 검사, 발급처 확인도 필요합니다. 다른 사람의 키를 검증 사이트에 붙여 넣거나 권한이 필요한 시험 요청에 사용하지 마세요.

bash
rg -l 'sk_live_|rk_live_' .next/static

실제 비밀 키를 찾았다면 무엇을 하나요?

발급처, 담당자, 현재 상태, 권한, 종속 작업을 확인합니다. 작동하는 비공개 자격증명이 공개 산출물에 있다면 대체 키를 준비하고 발급처에서 기존 값을 신속히 폐기합니다. 값을 공개한 코드 경로를 없애고 다시 빌드·배포한 뒤 접근 가능한 과거 배포와 복사본도 확인합니다. 노출 기간의 발급처 로그에서 사용 여부를 살핍니다. “악용 기록 없음”은 수집된 로그 범위에서만 해석해야 합니다.

의도적으로 공개하는 키를 스캐너의 API 키 패턴에 걸렸다는 이유만으로 교체할 필요는 없습니다. 제한 키도 가볍게 봐서는 안 됩니다. Stripe는 권한이 좁은 제한 키도 서버용 자격증명으로 분류합니다. 노출된 유효 비밀 키라면 대체 키로 작업이 돌아가고 이전 키는 더 이상 인증되지 않는지 확인합니다.

Cremit은 어디까지 도울 수 있나요?

Cremit은 지원하는 연결 소스에서 자격증명을 찾고, 지원 유형을 발급처에 확인하며, 제공 가능한 AWS 액세스 키·GCP API 키 정보를 보여주고 조치 담당자를 추적할 수 있습니다. 모든 브라우저 번들을 검사했거나 보이는 값이 악용됐거나 모든 발급처 키가 교체됐다는 사실을 입증하지는 않습니다. 연결 소스의 발견 결과를 자체 빌드 산출물, 배포 기록, 발급처 로그와 함께 봅니다.

함께 읽을 글

Vercel·Next.js 환경 변수가 브라우저로 나가는 경로

Git 시크릿 스캐닝: 패턴 일치와 작동하는 키 구분하기

근거 자료

Resend — 2024년 1월 10일 사고 보고서

Next.js — 환경 변수와 클라이언트 번들

Next.js — next.config.js env의 번들 포함

Stripe — API 키 종류와 보관 위치

Vercel — 환경 변수와 배포

네트워크에 공유해보세요LinkedInX

이어서 읽기

다음 글을 메일로 받아보세요

Cremit의 월간 자격증명 보안 브리프. 한 통에 핵심만 담습니다.

이메일을 판매하거나 외부에 넘기지 않으며, 언제든 수신거부할 수 있습니다.

프런트엔드 API 키 노출: JavaScript 점검법 | Cremit