출시 전 SaaS 보안 체크리스트: 우선 확인해야 할 40개 항목 (2026)

출시 전 SaaS 보안 체크리스트는 아직 갖지 못한 수많은 컴플라이언스 배지보다 훨씬 가치 있다. 불편한 진실은 이것이다: 대부분의 출시 체크리스트는 무엇을 보안해야 하는지 말할 뿐 어떻게 해야 하는지는 절대 보여주지 않는다. 이 글은 실제 코드를 제시한다. 우리는 Next.js와 Supabase로 구축하고 있으며, 단 하나의 빠진 tenant_id 필터로 테스트 계정이 다른 고객의 데이터를 읽을 수 있게 하는 것을 봐왔다. IBM의 2024년 데이터 침해 비용 보고서에 따르면 글로벌 평균은 $4.88M이다. 출시하기 위해 SOC 2가 필요하지는 않다. 아래의 애플리케이션 레이어 기본 사항이 필요하고, 이는 그룹화되고, 실행 가능하며, OWASP 및 NIST에 매핑되어 있다.

핵심 요점

  • SOC 2나 펜테스트 없이도 출시할 수 있다. 아래의 애플리케이션 레이어 기본 사항이 필요할 뿐이다.
  • 가장 위험한 출시 버그는 빠진 tenant_id 필터로 인한 교차 테넌트 데이터 유출이다.
  • 절대 인증을 직접 만들지 말 것. Auth.js, Clerk 또는 Supabase Auth를 사용하라.
  • 배포 전에 NEXT_PUBLIC_ 접두사를 감시할 것. 이것이 비밀을 유출하는 가장 빠른 방법이다.

이 기본 사항은 OWASP ASVS 5.0 및 NIST 보안 소프트웨어 개발 프레임워크(SSDF)에 매핑되며, 이는 Google이 이 주제에서 가장 신뢰하는 두 가지 참고 자료이면서도 최고 순위의 가이드들이 인용하기를 꺼리는 것들이다.

출시 전 보안 체크리스트 (빠른 버전)

다음은 출시 전 필요한 최소 보안 요구사항으로, 6가지 카테고리로 그룹화되어 있다. 총 40개 항목. 위에서부터 아래로 실행하고, 코드 섹션은 개발자에게 넘기며, 아래 우선순위 표에서 P0로 표시된 모든 사항을 출시 차단 사항으로 취급하라.

비밀 & 설정
- .env는 첫 번째 커밋부터 .gitignore에 있으며 절대 커밋되지 않았다.
- 모든 NEXT_PUBLIC_과 VITE_ 접두사를 감시했고, 브라우저로 전송되는 비밀이 없다.
- 서버 비밀은 매니저(플랫폼 환경 변수, AWS Secrets Manager, Vault)에 있으며 저장소에 없다.
- 깃 히스토리를 건드린 모든 키는 출시 전에 로테이션되었다.
- 로그, 오류 페이로드, 클라이언트 번들에 비밀이 나타나지 않는다.
- 빌드된 번들에서 라이브 키를 grep했다 ( grep -r "sk_live" .next/ ).

인증 & 접근
- 인증은 라이브러리(Auth.js, Clerk 또는 Supabase Auth)로 구축되었으며, 직접 구축하지 않았다.
- MFA는 계정에서 사용 가능하다.
- 세션 쿠키는 Secure, HttpOnly, SameSite를 설정한다.
- JWT나 세션 토큰이 localStorage에 저장되지 않는다.
- RBAC와 최소 권한 역할은 서버 측에서 강제되며, UI에서만 숨겨지지 않는다.
- 비밀번호는 Argon2 또는 bcrypt로 해시되었다 (자체 관리 인증인 경우에만).
- 비밀번호 재설정 및 이메일 확인 흐름이 악용에 대해 테스트되었다.

데이터 & 테넌트
- 모든 쿼리에는 tenant_id 필터가 있다.
- 테넌트 스코핑은 ORM 또는 저장소 레이어에서 강제되며, 쿼리마다 기억되지 않는다.
- 행 레벨 보안이 활성화되어 있고 그 실패 모드를 이해한다.
- 모든 객체 ID 엔드포인트는 소유권 확인을 실행한다 (이것이 IDOR을 제거한다).
- tenant_id는 캐시 키 및 객체 저장소 경로에 포함된다.
- 데이터는 저장 시와 전송 중에 암호화된다.
- 결제 및 웹훅 서명(Stripe 등)은 서버 측에서 검증된다.

종속성 & 공급망
- npm audit 또는 pnpm audit는 높음 및 심각 사항이 없거나 명시적으로 검토되었다.
- Dependabot 또는 Renovate가 활성화되었다.
- Snyk 또는 Socket이 더 심층적인 SCA 및 악성코드와 라이선스 확인을 실행한다.
- 잠금 파일이 커밋되었다.
- 중단되거나 관리되지 않는 패키지가 중요 경로에 없다.
- Docker를 배포하는 경우 컨테이너 이미지가 스캔된다.

네트워크 & 전송
- HTTPS는 HSTS 프리로드와 함께 모든 곳에서 강제된다.
- Content-Security-Policy가 설정되어 있다 (먼저 보고서 전용, 그 다음 강제).
- X-Content-Type-Options: nosniff와 X-Frame-Options / frame-ancestors가 설정되어 있다.
- Referrer-Policy와 Permissions-Policy가 설정되어 있다.
- CORS는 화이트리스트를 사용하며, 절대 *와 자격증명을 함께 사용하지 않는다.
- 속도 제한이 인증 및 비용이 많이 드는 엔드포인트를 보호한다.
- 모든 엔드포인트는 스키마(Zod 또는 유사)로 입력을 검증한다.

모니터링 & 대응
- 중앙 집중식 감사 로그는 누가 무엇에 언제 접근했는지 기록한다.
- 오류 처리는 사용자에게 스택 추적을 절대 유출하지 않는다.
- 인증 이상(로그인 실패 급증, 불가능한 이동)에 대해 경고가 발동한다.
- 자동화된 백업이 실행되고 복원을 테스트했다.
- 사건 대응 담당자와 한 페이지 운영서가 있다.
- 가동 시간 및 오류 모니터링(Sentry 또는 동등)이 실행 중이다.
- 펜테스트를 수행할 트리거를 알고 있다.

우선순위별 수정

모든 항목이 출시를 차단하지는 않는다. 이 분류 표는 기본 사항을 손상(건너뛴 경우)별로 정렬하여 창업자가 협상 불가능한 사항을 알 수 있도록 한다. P0 = 출시 전 수정, P1 = 첫 주 내 수정, P2 = 분기 내 수정.

비밀 & 설정: 클라이언트 번들로 유출되는 키가 있나?

출시 시 비밀 위생은 어떤 자격증명도 브라우저에 도달하지 않음을 의미한다. Next.js의 NEXT_PUBLIC_ 접두사 (그리고 Vite의 VITE_)는 모든 방문자에게 값을 전송하므로, 한 번의 잘못된 접두사는 키를 유출한다. .env를 깃 밖으로 빼고, 서버 비밀을 매니저에 넣고, 배포 전에 빌드 출력을 grep하라.

우리가 가장 많이 보는 함정은 다음과 같다: NEXT_PUBLIC_는 "공개 정보"를 의미하지 않는다. 이것은 "나는 모든 방문자의 브라우저로 이것을 문자 그대로 배송하고 있다"를 의미한다. Stripe 비밀이나 서비스 역할 키에 이 접두사를 붙이면 DevTools를 열은 누구나 번들에서 라이브로 사용할 수 있게 된다.

비밀 기본 사항의 나머지는 지루하고 협상 불가능하다: 첫 번째 커밋부터 .gitignore에 .env, 저장소 대신 매니저에 서버 비밀, 깃 히스토리를 건드린 모든 키의 로테이션 (커밋을 삭제해도 유출이 취소되지 않는다). 유출된 배포 토큰은 정확히 Vercel 사건 같은 침해가 시작되는 방식이므로, 모든 토큰을 이미 누군가의 감시 목록에 있는 것처럼 취급하라.

인증 & 접근: 인증을 구축해야 할까, 아니면 라이브러리를 사용해야 할까?

인증을 구축해야 할까, 아니면 라이브러리를 사용해야 할까? 거의 항상 라이브러리를 사용하라. Auth.js, Clerk, Supabase Auth는 당신이 프로덕션에서 다시 발견할 수 있는 수년의 엣지 케이스를 흡수했다: 세션 고정, 토큰 취소, 재설정 흐름 악용. 직접 구축하는 것은 보안 엔지니어와 공급자가 맞지 않는 이유가 있을 때만 방어할 수 있으며, 이는 드물다.

직접 인증을 구축하는 것은 월 25달러를 절약하는 가장 비싼 방법이다. 정직한 옵션이 어떻게 비교되는지는 다음과 같다.

라이브러리를 선택했지만 설정을 건너뛴 팀을 침몰시키는 두 가지 함정이 있다. 첫째, JWT 취소는 매우 어려워서 도난당한 토큰이 만료될 때까지 유효하게 유지되므로, 토큰 수명을 짧게 유지하고 민감한 모든 것에 서버 측 세션을 선호하라. 둘째, localStorage의 토큰은 모든 XSS 페이로드에 의해 도난당할 수 있으므로, Secure 및 SameSite를 사용하여 httpOnly 쿠키에 세션을 저장하라. UI에서 버튼을 숨기는 것이 아니라 서버에서 RBAC을 강제하라.

당신의 SaaS가 AI 또는 LLM 기능을 포함한다면, 모델 입력도 신뢰할 수 없는 인증 경계로 취급하라. 프롬프트 주입 방지에 대한 우리의 가이드를 보라, 왜냐하면 도구 접근 권한을 가진 탈옥당한 보조는 채팅 창을 입고 있는 접근 제어 문제이기 때문이다.

데이터 & 테넌트: 한 테넌트가 다른 테넌트의 데이터를 읽지 않도록 어떻게 중지할까?

테넌트 격리는 모든 쿼리, 캐시 키, 저장소 경로가 현재 테넌트로 스코핑되는 것을 의미한다. 빠진 tenant_id 필터는 한 고객이 다른 고객의 데이터를 읽을 수 있도록 하며, 이는 가장 위험한 출시 버그이다. 행 레벨 보안이 도움이 되지만, 이것은 안전벨트이지 력장이 아니므로, 모든 객체 ID 엔드포인트에도 소유권 확인을 추가하라.

이것은 경쟁사가 코드로 다루지 않는 섹션이며, 이것이 교차 테넌트 유출이 프로덕션에 빠져나가는 이유이다. 수정은 ID 자체를 절대 신뢰하지 않음으로 시작한다. 모든 읽기를 호출자의 테넌트로 스코핑하고, 데이터 레이어에서 강제하므로 아무도 쿼리마다 기억할 필요가 없다.

관련 실패는 IDOR (불안한 직접 객체 참조)이며, OWASP API 보안 Top 10은 이것을 API1: 깨진 객체 레벨 인증으로 분류한다. 테스트 계정은 URL의 ID를 증가시키고 절대 볼 수 없는 기록을 읽는다. PortSwigger의 Web Security Academy는 공격자가 이들을 어떻게 찾는지에 대한 전체 워크스루를 가지고 있다. 수정은 하나의 소유권 확인이다.

당신을 정직하게 유지하는 프레임은 다음과 같다: 모든 IDOR은 테넌트 격리 실패이지만, 모든 테넌트 격리 실패가 IDOR은 아니다. Postgres 행 레벨 보안은 데이터베이스에서 많은 것을 잡지만, 이것은 당신이 그것에 의존하기 전에 알 가치가 있는 무음 실패 모드를 가지고 있다.

...

출처 바로가기