Web Security 실전 가이드북

이 가이드북을 쓰는 법

이 문서는 Web Security 문서 묶음을 다시 요약하는 문서가 아니다.

실제 개발 중 애매한 상황을 만났을 때 무엇부터 보고, 어떤 순서로 판단하고, 어느 문서를 다시 열어야 하는지 안내하는 작업용 지도다.

각 Guide는 하나의 실전 상황에서 시작한다. 먼저 증거를 보고, 판단 순서를 잡은 뒤, 관련 문서로 돌아가도록 구성했다.

좋은 사용법은 간단하다.

  • 지금 겪는 상황과 가장 가까운 Guide를 고른다.
  • 먼저 판단할 것에서 아직 모르는 값을 표시한다.
  • 확인할 증거를 실제 코드, 로그, 요청/응답에서 찾는다.
  • 찾아볼 문서를 열어 세부 기준을 확인한다.
  • 마지막에 가져갈 한 문장을 자신의 체크리스트로 바꾼다.

커버리지 지도

기존 문서연결되는 Guide다시 봐야 하는 이유
Web Security 전체 지도 압축 정리Guide 1, 14전체 요청 흐름과 학습 순서를 잡는 출발점이다.
웹 요청과 보안 경계 전체 흐름Guide 1, 11, 14HTTP 요청이 어떤 경계를 지나며 신뢰 수준이 바뀌는지 확인한다.
공격자 관점으로 보는 웹 서비스Guide 1, 12, 14정상 요청에서 공격자가 바꿀 수 있는 값을 찾는 연습에 필요하다.
인증 인가 브라우저 API 보안 연결Guide 2, 5, 6, 14인증, 인가, 브라우저 정책, API 보안이 한 요청에서 연결되는 지점을 본다.
개인 프로젝트와 기업 보안 수준 비교Guide 13, 14작은 서비스와 운영 서비스에서 보안 기본선을 어떻게 조절할지 판단한다.
Web Security 학습 로드맵Guide 14학습 순서를 실전 점검 루틴으로 바꾸는 기준이다.
Threat Model Trust Boundary 압축 정리Guide 1, 11자산, actor, entry point, trust boundary를 한 번에 정리한다.
Asset Actor Entry PointGuide 1, 3, 11보호 자산과 진입점을 놓치면 이후 테스트가 흐려진다.
Trust Boundary와 Attack SurfaceGuide 1, 6, 11어느 값이 어느 경계에서 검증되어야 하는지 정한다.
STRIDE와 웹 백엔드 위협 모델링Guide 1, 11추상적인 걱정을 테스트와 로그 항목으로 바꾼다.
보안 요구사항을 설계에 반영하는 법Guide 1, 13보안 요구사항을 API 계약, 코드, 테스트로 내려보낸다.
보안 리뷰 질문지Guide 12, 14리뷰에서 물어야 할 질문을 구체화한다.
Authentication과 Identity 압축 정리Guide 2인증 흐름 전체를 빠르게 다시 잡는다.
Authentication Identity PrincipalGuide 2, 5principal과 identity를 혼동하지 않게 한다.
Login Flow와 Credential 검증Guide 2, 12로그인 실패 응답, credential 검증, 계정 열거 위험을 점검한다.
Session 기반 인증Guide 2, 4session id, cookie, 폐기 정책을 함께 판단한다.
MFA와 계정 탈취 방어Guide 2, 10탈취 의심 계정의 step-up과 MFA 이벤트를 확인한다.
로그인 실패 Abuse와 운영 로그Guide 2, 10credential stuffing과 장애를 로그로 구분한다.
Session Cookie JWT Token 압축 정리Guide 4session, cookie, JWT, access/refresh token의 차이를 잡는다.
Session과 Cookie 보안 모델Guide 2, 4, 6cookie 속성과 CSRF/XSS 영향을 함께 본다.
JWT 구조와 검증Guide 4, 12issuer, audience, signature, 만료 검증을 점검한다.
Access Token Refresh Token 설계Guide 4, 10token 탈취 사고에서 폐기 범위를 정한다.
Token 저장 위치와 탈취 위험Guide 4, 6, 10localStorage, cookie, memory 저장의 공격면을 비교한다.
Logout Revocation RotationGuide 4, 10logout과 사고 대응용 revocation을 구분한다.
Authorization RBAC ABAC Permission 압축 정리Guide 3권한 모델 전체 구조를 빠르게 다시 잡는다.
Authentication과 Authorization 분리Guide 3, 5, 12로그인 성공과 resource 접근 허용을 분리한다.
RBAC Role PermissionGuide 3, 12role과 permission, meta-permission을 구분한다.
ABAC와 Resource OwnershipGuide 3, 5소유권과 context 기반 권한 검사를 설계한다.
Method Security와 API 권한 검사Guide 3, 8, 12URL rule 밖의 Service 경계를 점검한다.
권한 감사 로그와 우회 위험Guide 3, 10, 12권한 변경과 권한 거부가 나중에 복원되는지 확인한다.
Browser Security 압축 정리Guide 6SOP, CORS, CSRF, XSS, SameSite, CSP의 역할을 한 번에 본다.
Same-Origin Policy와 브라우저 보안 모델Guide 6브라우저가 막는 것과 서버가 직접 막아야 하는 것을 나눈다.
CORS Preflight와 실전 디버깅Guide 6, 13CORS 오류인지 보안 사고인지 구분한다.
CSRF 공격 원리와 방어 설계Guide 6, 10cookie 자동 전송과 상태 변경 요청을 함께 본다.
XSS 공격 원리와 출력 저장 DOM 방어Guide 6, 10출력 context, token 저장 위치, 사고 대응을 연결한다.
SameSite CSP 보안 헤더Guide 6, 13보안 header가 실제로 어떤 피해를 줄이는지 확인한다.
API Security SQL Injection Validation 압축 정리Guide 5API 입력 검증과 SQLi, Mass Assignment, 오류, abuse를 묶어 본다.
SQL Injection 공격과 Prepared StatementGuide 5, 12값 binding과 query structure allowlist를 구분한다.
Input Validation과 Output EncodingGuide 5, 6입력 검증과 출력 인코딩을 섞지 않는다.
Mass Assignment와 DTO 경계Guide 5, 8DTO가 endpoint별 allowlist인지 확인한다.
Error Message 정보 노출 방지Guide 5, 12외부 응답과 내부 로그의 정보 수준을 나눈다.
Rate Limit과 Abuse 방어Guide 5, 10, 13반복 호출과 업무 자원 소모를 제한한다.
OAuth2 OIDC Social Login 압축 정리Guide 7OAuth2와 OIDC의 경계를 빠르게 다시 잡는다.
OAuth2 역할과 Authorization Code FlowGuide 7Authorization Code Flow의 actor와 channel을 확인한다.
PKCE State Redirect URI 보안Guide 7, 12callback 혼동, code 탈취, redirect URI 문제를 점검한다.
OIDC ID Token과 UserInfoGuide 7ID Token과 UserInfo를 내부 권한으로 바로 쓰지 않는다.
Social Login 계정 연결 위험Guide 7, 10소셜 로그인 성공과 계정 연결을 분리한다.
Token 교환 저장 갱신 운영Guide 7, 10provider token 저장, 갱신, 폐기, 로그를 확인한다.
Password Hashing Secret Crypto 압축 정리Guide 9password hashing, secret, crypto, TLS를 한 번에 정리한다.
Password Hashing Salt Work FactorGuide 2, 9비밀번호 저장과 work factor migration을 판단한다.
bcrypt Argon2 PBKDF2 선택 기준Guide 9운영 가능한 password hashing 알고리즘을 고른다.
Secret 환경 변수 Vault RotationGuide 9, 10, 13secret 저장, 접근, 회전, 유출 대응을 본다.
암호화와 서명 실수Guide 9암호화와 서명의 목적을 혼동하지 않는다.
TLS 인증서와 키 관리 기본Guide 9, 13인증서와 key lifecycle을 운영 기준으로 본다.
Security Log Audit Abuse Detection 압축 정리Guide 10보안 이벤트와 감사 로그의 전체 구조를 잡는다.
Audit Log와 Security EventGuide 10, 12감사 기록과 탐지 신호를 구분한다.
로그인 권한 변경 결제 이벤트 추적Guide 10여러 이벤트의 순서를 복원한다.
이상 요청 Abuse DetectionGuide 10, 13정상 기능 남용 패턴과 대응 action을 설계한다.
개인정보 마스킹과 로그 보존Guide 10, 13로그가 증거이면서 유출 지점이 되지 않게 한다.
보안 사고 초기 대응Guide 10증거 보존과 containment의 순서를 잡는다.
API 보안 체크리스트 압축 정리Guide 11, 12, 13설계, 구현, 테스트, 배포, 리뷰 체크를 연결한다.
API 설계 전 보안 체크리스트Guide 1, 11API 계약 전에 보안 질문을 넣는다.
구현 중 보안 체크리스트Guide 5, 8, 11Controller, Service, Repository 경계를 구현 중에 확인한다.
테스트와 취약점 검증 체크리스트Guide 11, 12실패해야 하는 요청과 남아야 하는 로그를 테스트한다.
배포 전 운영 보안 체크리스트Guide 13production 설정, secret, rollback, monitoring을 확인한다.
코드 리뷰 보안 질문Guide 12diff를 공격 요청으로 읽는다.
Spring Security는 어디까지 책임지는가 압축 정리Guide 8Spring Security의 책임과 애플리케이션 책임을 나눈다.
Security FilterChain의 책임Guide 8, 13FilterChain이 맡는 첫 방어선을 확인한다.
Authentication Provider SecurityContextGuide 2, 8Authentication과 SecurityContext의 범위를 확인한다.
Authorization Manager Method SecurityGuide 3, 8AuthorizationManager와 Method Security의 역할을 본다.
Gateway WAF Infra와 책임 경계Guide 8, 13앞단 인프라와 애플리케이션 책임을 나눈다.
Spring Security로 해결되지 않는 보안 문제Guide 8, 11프레임워크 밖에 남는 입력, 데이터, 로그 책임을 확인한다.
Web Security 실전 플레이북 압축 정리Guide 10, 14사고 대응을 탐지, 증거, containment, 재발 방지로 바꾼다.
로그인 장애와 계정 탈취 의심 대응Guide 2, 10로그인 장애와 계정 탈취 시도를 구분한다.
Token 탈취와 세션 무효화 대응Guide 4, 10token family와 기기 세션 기준으로 폐기 범위를 잡는다.
권한 우회 취약점 대응Guide 3, 10권한 우회 사고의 영향 범위를 산정한다.
CORS CSRF XSS 사고 대응Guide 6, 10브라우저 보안 사고의 containment를 나눈다.
보안 사고 보고서와 재발 방지Guide 10, 14사고 기록을 재발 방지 항목으로 바꾼다.

실전 Guide

Guide 1. 새 API를 만들기 전에 보안 경계부터 그린다

상황

팀에서 POST /teams/{teamId}/invitations API를 만들기로 했다.

기능 요구사항은 단순하다. 팀 관리자가 이메일을 입력하면 초대 메일이 발송되고, 받은 사람은 링크로 가입한다.

하지만 이 기능은 초대 token, role, 이메일, 팀 권한, 만료 시간, 감사 로그가 모두 얽힌다. 바로 Controller부터 만들면 나중에 “누가 누구를 어떤 권한으로 초대할 수 있는가”가 흐려진다.

먼저 판단할 것

  • 보호 자산은 초대 token인가, 팀 멤버십인가, role 변경 권한인가?
  • actor는 팀 관리자, 일반 멤버, 초대받은 사용자, 공격자, 메일 시스템 중 누구인가?
  • entry point는 초대 생성 API만인가, 초대 수락 URL과 재발송 API도 포함하는가?
  • 요청 body의 roleemail은 어느 경계에서 검증되는가?
  • 실패했을 때 어떤 로그가 남아야 나중에 초대 남용을 복원할 수 있는가?

확인할 증거

  • API 설계 문서에 actor, resource, action이 분리되어 있는지 본다.
  • 초대 token이 응답, 로그, 메일 템플릿, trace에 원문으로 남는지 본다.
  • role 값이 client 요청에서 온 값인지 서버 policy에서 제한되는 값인지 본다.
  • 초대 수락 시 이미 가입된 계정과 새 계정의 흐름이 갈라지는지 본다.
  • 초대 생성, 수락, 만료, 취소가 감사 로그로 남는지 본다.

진행 순서

  1. 먼저 asset, actor, entry point를 표로 쓴다.
  2. 각 entry point에서 신뢰 경계를 표시한다.
  3. STRIDE 또는 abuse case로 조작 가능한 값을 찾는다.
  4. 보안 요구사항을 API 계약과 테스트 이름으로 바꾼다.
  5. 구현 전 체크리스트에 owner, token, role, log 항목을 넣는다.

찾아볼 문서

흔한 실수

  • 초대 생성 API만 보고 초대 수락, 재발송, 취소, 만료를 놓친다.
  • role을 enum validation만 하고 권한 정책으로 검증하지 않는다.
  • 초대 token을 로그에 남겨 운영 로그가 2차 유출 지점이 된다.
  • “관리자만 호출한다”는 말로 팀 소유권과 관리자 범위를 생략한다.

정리

새 API의 보안은 구현 뒤에 붙이는 장식이 아니다. 기능 요구사항이 생기는 순간 actor, asset, entry point, trust boundary를 같이 잡아야 한다.

가져갈 한 문장

API를 만들기 전에는 URL보다 먼저 “누가 어떤 자산에 어떤 권한으로 닿는가”를 적는다.

Guide 2. 로그인 장애가 터졌을 때 공격과 장애를 나눈다

상황

오전 9시 20분부터 로그인 실패율이 급증했다.

고객지원에는 “비밀번호가 맞는데 로그인이 안 된다”는 문의가 들어오고, 보안 알림에는 여러 국가 IP에서 실패 이벤트가 보인다.

바로 계정을 잠그면 정상 사용자 피해가 커질 수 있고, 단순 장애로 보면 credential stuffing을 놓칠 수 있다.

먼저 판단할 것

  • 실패가 특정 내부 의존성 장애로 몰리는가, 여러 계정과 출처에 분산되는가?
  • 실패 후 성공한 계정이 있는가?
  • MFA challenge 반복이나 새 기기 등록이 이어지는가?
  • session과 refresh token을 함께 무효화해야 하는 계정이 있는가?
  • 사용자 공지 전에 확정된 사실과 추정이 분리되어 있는가?

확인할 증거

  • auth.login.failed, auth.login.succeeded, mfa.challenge, password.changed 로그를 시간순으로 본다.
  • 계정별, IP별, ASN별, user agent별 실패 분포를 본다.
  • 동일 credential pair 반복인지 password spraying인지 구분한다.
  • 인증 서버, DB, 외부 OAuth provider, SMS/Mail provider 장애 지표를 확인한다.
  • 보호 조치 후 기존 session과 token이 실제로 거부되는지 본다.

진행 순서

  1. 장애 가능성과 공격 가능성을 동시에 열어 둔다.
  2. 내부 의존성 장애 지표를 먼저 확인한다.
  3. 실패 이벤트를 계정, 출처, credential 패턴으로 묶는다.
  4. 실패 후 성공과 민감 변경 이벤트를 연결한다.
  5. 위험 계정만 우선 보호하고 전체 잠금은 마지막 선택지로 남긴다.
  6. 사용자가 해야 할 조치와 아직 확인 중인 사실을 분리해 공지한다.

찾아볼 문서

흔한 실수

  • 실패 횟수만 보고 모든 계정을 잠근다.
  • 장애와 공격 로그를 같은 이벤트 이름으로 남긴다.
  • 비밀번호 재설정만 요구하고 기존 session과 refresh token을 방치한다.
  • MFA가 있으니 탈취 위험이 낮다고 단정한다.

정리

로그인 장애 대응은 인증 기능만 보는 일이 아니다. credential, MFA, session, token, 사용자 공지, 고객지원까지 하나의 흐름으로 판단해야 한다.

가져갈 한 문장

로그인 실패가 늘면 먼저 “장애인가, 공격인가, 둘 다인가”를 증거로 나눈다.

Guide 3. 권한 우회가 의심되면 패치보다 영향 범위를 먼저 잡는다

상황

사용자 A가 /api/orders/9001을 호출했는데 사용자 B의 주문이 보였다는 제보가 들어왔다.

개발자는 @PreAuthorize를 추가하면 된다고 말한다.

하지만 이미 누가 어떤 주문을 읽었고, 수정 API도 같은 문제가 있는지 모르면 패치만으로 끝낼 수 없다.

먼저 판단할 것

  • 이 문제는 인증 실패인가, 인가 실패인가?
  • URL rule, Method Security, Service policy, Repository 조건 중 어디가 비어 있는가?
  • 목록, 상세, export, batch, 관리자 대리 조회가 같은 권한 조건을 쓰는가?
  • 영향 범위는 endpoint 기준인가, resource type과 action 기준인가?
  • 거부 로그와 허용 로그로 이미 노출된 데이터를 복원할 수 있는가?

확인할 증거

  • 정상 사용자와 비소유자 사용자의 요청/응답을 비교한다.
  • actor, resource, action, owner, result, reason 로그가 있는지 본다.
  • Repository가 findById만 쓰는지 findByIdAndOwnerId 같은 조건을 쓰는지 본다.
  • 관리자, 고객지원, batch, 내부 API가 같은 Service policy를 통과하는지 본다.
  • 사고 기간 동안 200 응답 중 owner mismatch가 있었는지 역추적한다.

진행 순서

  1. 인증 성공과 권한 허용을 분리한다.
  2. 영향 resource와 action 목록을 먼저 산정한다.
  3. 임시 containment로 위험 endpoint나 export를 제한한다.
  4. Service policy와 Repository 조건을 함께 수정한다.
  5. 비소유자 테스트를 CI에 넣는다.
  6. 사고 보고서에 노출 범위와 남은 불확실성을 남긴다.

찾아볼 문서

흔한 실수

  • isAuthenticated()를 권한 검사로 착각한다.
  • Controller annotation만 추가하고 Repository 조건을 그대로 둔다.
  • 관리자 예외 경로를 “운영자만 쓰니까 안전하다”고 본다.
  • 영향 범위 산정 없이 “수정 완료”라고 보고한다.

정리

권한 우회 대응의 중심은 코드 한 줄이 아니라 actor, resource, action, owner, log를 같은 정책으로 묶는 일이다.

가져갈 한 문장

권한 우회는 패치 전에 “누가 무엇을 봤는가”부터 복원한다.

Guide 4. JWT와 refresh token을 쓰는 서비스의 로그아웃을 설계한다

상황

서비스가 JWT 기반 인증으로 전환했다.

access token은 15분, refresh token은 14일이다. 사용자는 “전체 기기 로그아웃” 기능을 요구하고, 보안팀은 refresh token 재사용 탐지를 원한다.

단순히 브라우저 저장소를 비우는 것으로는 충분하지 않다.

먼저 판단할 것

  • access token과 refresh token의 저장 위치는 어디인가?
  • refresh token은 rotation되는가, token family가 있는가?
  • logout은 현재 기기만 종료하는가, 전체 기기를 종료하는가?
  • 비밀번호 변경, MFA 변경, 탈취 의심 시 token 폐기 범위가 다른가?
  • resource server가 폐기된 token을 실제로 거부하는가?

확인할 증거

  • JWT의 issuer, audience, exp, nbf, alg, key id 검증 코드를 본다.
  • refresh token 저장소에 원문이 아니라 hash와 family id가 있는지 본다.
  • token rotation 후 이전 token 재사용이 사고 이벤트로 남는지 본다.
  • logout API가 서버 저장소를 갱신하는지 본다.
  • access token 폐기 지연과 cache TTL을 확인한다.

진행 순서

  1. session 기반인지 token 기반인지 먼저 분리한다.
  2. access token과 refresh token의 책임을 나눈다.
  3. refresh token family와 device session 모델을 설계한다.
  4. logout, revocation, rotation, password change 이벤트를 연결한다.
  5. 사고 대응 시 token 폐기와 사용자 재로그인 UX를 함께 준비한다.

찾아볼 문서

흔한 실수

  • JWT는 stateless라서 logout이 필요 없다고 말한다.
  • refresh token을 원문으로 DB와 로그에 남긴다.
  • 현재 기기 로그아웃과 전체 기기 로그아웃을 같은 동작으로 구현한다.
  • token 폐기 후 resource server에서 실제 거부되는지 확인하지 않는다.

정리

token 기반 인증에서는 발급보다 폐기와 재사용 탐지가 더 어렵다. 처음부터 token family, device session, revocation event를 설계해야 한다.

가져갈 한 문장

JWT 전환의 핵심 질문은 “어떻게 발급할까”보다 “어떻게 끊을까”다.

Guide 5. 요청 DTO를 만들 때 보안 경계를 함께 만든다

상황

프로필 수정 API를 만들고 있다.

프론트 화면에는 nickname만 있지만, 응답 JSON에는 role, point, emailVerified도 보인다.

개발자는 빠르게 구현하려고 entity를 request body로 받으려 한다.

먼저 판단할 것

  • 이 endpoint에서 사용자가 바꿀 수 있는 필드는 정확히 무엇인가?
  • request DTO가 allowlist 역할을 하는가?
  • unknown field는 무시할 것인가, 거부할 것인가?
  • Service가 업무 의미를 가진 domain method를 호출하는가?
  • 민감 필드 변경은 별도 관리자 endpoint와 감사 로그를 가지는가?

확인할 증거

  • Controller가 entity를 직접 받는지 본다.
  • DTO에 사용자가 바꿀 수 없는 필드가 들어 있는지 본다.
  • mapper가 명시적으로 필드를 복사하는지 본다.
  • Repository 저장 전 권한과 소유권 검사가 있는지 본다.
  • 실패 응답과 로그가 민감값을 노출하지 않는지 본다.

진행 순서

  1. endpoint별 허용 필드 목록을 만든다.
  2. request DTO를 허용 필드만 담는 allowlist로 만든다.
  3. Bean Validation으로 형식을 검증한다.
  4. Service에서 소유권, 상태 전이, 업무 의미를 검증한다.
  5. Repository에서 데이터 범위를 좁힌다.
  6. 화면에 없는 필드를 넣는 테스트를 추가한다.

찾아볼 문서

흔한 실수

  • DTO를 쓰면서도 entity의 모든 필드를 그대로 복사한다.
  • @Valid가 권한 필드 오염을 막아 준다고 착각한다.
  • blocklist로 민감 필드를 막으려다 새 필드가 추가될 때 빠뜨린다.
  • 사용자용 endpoint와 관리자용 endpoint가 같은 DTO를 공유한다.

정리

request DTO는 단순한 데이터 운반 객체가 아니라 사용자가 바꿀 수 있는 필드의 계약이다.

가져갈 한 문장

DTO는 편의 객체가 아니라 endpoint별 입력 allowlist다.

Guide 6. CORS 오류가 났을 때 설정부터 열지 않는다

상황

프론트 개발자가 “CORS 때문에 API가 안 된다”고 말한다.

급한 마음에 allowedOrigins("*")를 넣고 싶은데, 해당 API는 사용자 결제 정보와 프로필을 반환한다.

이 문제가 단순 개발 환경 오류인지, credential 포함 CORS 위험인지 먼저 구분해야 한다.

먼저 판단할 것

  • 요청 Origin은 무엇이고, credential이 포함되는가?
  • preflight가 실패하는가, 실제 요청이 실패하는가?
  • 응답을 외부 Origin의 스크립트가 읽을 수 있는가?
  • SameSite cookie, CSRF token, Authorization header 중 어떤 인증 방식을 쓰는가?
  • XSS와 token 저장 위치가 이 CORS 판단에 영향을 주는가?

확인할 증거

  • 브라우저 Network 탭에서 Origin, method, request headers를 확인한다.
  • preflight OPTIONS 응답과 실제 API 응답을 분리해 본다.
  • Access-Control-Allow-Origin, Allow-Credentials, Vary: Origin을 확인한다.
  • Spring Security CORS 설정과 gateway CORS 설정이 중복되지 않는지 본다.
  • CSRF 실패인지 CORS 실패인지 서버 로그 event를 확인한다.

진행 순서

  1. 브라우저가 막은 것인지 서버가 거부한 것인지 구분한다.
  2. credential 포함 여부를 확인한다.
  3. 허용 Origin을 정확한 scheme, host, port로 제한한다.
  4. 상태 변경 요청은 CSRF 방어와 함께 본다.
  5. XSS 가능성과 token 저장 위치를 같이 판단한다.
  6. 운영 배포 전 CORS와 보안 header를 다시 검증한다.

찾아볼 문서

흔한 실수

  • CORS를 서버 간 접근 제어로 이해한다.
  • preflight 성공을 인증/인가 성공으로 착각한다.
  • credential 포함 API에 동적 Origin 반사를 넣는다.
  • CSRF 실패와 CORS 실패를 같은 403으로만 남긴다.

정리

CORS 문제는 개발 편의 설정이 아니라 브라우저가 민감 응답을 어떤 Origin의 스크립트에 노출할지 정하는 문제다.

가져갈 한 문장

CORS를 열기 전에 “누가 응답을 읽게 되는가”를 먼저 묻는다.

Guide 7. 소셜 로그인을 붙일 때 로그인과 계정 연결을 분리한다

상황

서비스에 Google, GitHub 로그인을 붙이기로 했다.

기획은 “이메일이 같으면 기존 계정으로 로그인시키자”고 한다.

그런데 OAuth2 로그인 성공과 내부 계정 연결은 같은 일이 아니다.

먼저 판단할 것

  • OAuth2 인증 결과는 어떤 provider와 subject에서 왔는가?
  • OIDC ID Token의 issuer, audience, nonce, exp가 검증되는가?
  • email은 verified 상태인가, 내부 계정 연결 근거로 충분한가?
  • 기존 로그인 사용자가 provider 계정을 연결할 때 재인증이 필요한가?
  • provider access token과 내부 session을 섞고 있지 않은가?

확인할 증거

  • callback의 state, code, redirect_uri 검증을 본다.
  • PKCE code verifier가 token 교환 시점에 쓰이는지 본다.
  • (provider, subject) unique constraint가 있는지 본다.
  • 계정 연결, 해제, 실패, 중복 연결 시도가 감사 로그로 남는지 본다.
  • provider token이 원문으로 로그나 DB에 남는지 본다.

진행 순서

  1. OAuth2와 OIDC의 목적을 나눈다.
  2. 로그인 flow와 계정 연결 flow를 별도 intent로 둔다.
  3. state, PKCE, redirect URI를 저장 상태와 묶어 검증한다.
  4. ID Token과 UserInfo claim을 내부 권한으로 바로 쓰지 않는다.
  5. provider token 저장과 갱신, 폐기 정책을 정한다.
  6. 연결/해제 이벤트를 사용자 알림과 감사 로그로 남긴다.

찾아볼 문서

흔한 실수

  • email만 보고 내부 계정에 자동 연결한다.
  • 소셜 로그인 성공을 내부 권한 부여로 바로 해석한다.
  • 계정 연결에 최근 재인증을 요구하지 않는다.
  • provider access token을 내부 API access token처럼 사용한다.

정리

OAuth2/OIDC는 외부 identity를 확인하는 흐름이고, 내부 계정 연결은 별도의 보안 민감 작업이다.

가져갈 한 문장

소셜 로그인에서 가장 위험한 순간은 로그인 성공이 아니라 내부 계정에 연결하는 순간이다.

Guide 8. Spring Security를 넣었는데도 남는 책임을 표로 만든다

상황

프로젝트에 Spring Security 설정이 들어갔다.

SecurityFilterChain, JWT filter, @PreAuthorize, CORS 설정도 있다.

그런데 프로필 수정 API는 entity를 그대로 받고, 검색 API는 문자열 query를 만들며, 로그에는 token 일부가 남는다.

먼저 판단할 것

  • Spring Security가 맡는 책임은 무엇인가?
  • 애플리케이션 Service가 직접 맡아야 하는 업무 권한은 무엇인가?
  • Repository가 데이터 범위를 좁히는가?
  • Gateway/WAF가 막는 것과 못 막는 것은 무엇인가?
  • 로그, secret, abuse detection은 어느 팀과 코드가 책임지는가?

확인할 증거

  • SecurityFilterChain matcher 순서와 public endpoint를 본다.
  • AuthenticationProvider가 만든 principal에 어떤 값이 들어가는지 본다.
  • Method Security가 self-invocation이나 batch 경로에서도 적용되는지 본다.
  • DTO, Repository, audit log가 Spring Security 설정과 같은 정책을 말하는지 본다.
  • Gateway/WAF 로그와 애플리케이션 로그가 trace id로 연결되는지 본다.

진행 순서

  1. Spring Security가 맡는 인증, 공통 인가, CSRF/CORS/header 책임을 적는다.
  2. 애플리케이션이 맡는 resource ownership과 업무 상태 전이를 적는다.
  3. Repository가 맡는 tenant/member 조건을 적는다.
  4. 운영이 맡는 secret, log, monitoring, WAF 책임을 적는다.
  5. endpoint별로 책임 표를 만든다.
  6. 책임이 비어 있는 칸을 테스트와 이슈로 바꾼다.

찾아볼 문서

흔한 실수

  • Spring Security가 켜져 있으니 입력 검증도 해결됐다고 생각한다.
  • URL matcher만 보고 Service와 Repository 조건을 보지 않는다.
  • WAF 차단을 애플리케이션 권한 검사의 대체재로 본다.
  • 보안 책임이 팀 사이에서 떠다니는데 문서화하지 않는다.

정리

Spring Security는 강력한 공통 경계다. 하지만 업무 소유권, 입력 allowlist, 데이터 범위, 운영 로그는 애플리케이션이 명시적으로 책임져야 한다.

가져갈 한 문장

Spring Security를 넣은 뒤에는 “무엇을 맡겼고 무엇이 남았는가”를 표로 남긴다.

Guide 9. 비밀번호, secret, key를 같은 방식으로 다루지 않는다

상황

서비스 보안 점검에서 세 가지 질문이 동시에 나왔다.

비밀번호 hash 알고리즘은 무엇인가, JWT signing key는 어디에 있는가, 외부 API key는 언제 회전되는가.

셋 다 “비밀값”처럼 보이지만 처리 방식은 다르다.

먼저 판단할 것

  • 비밀번호는 복호화할 값인가, 검증용 verifier인가?
  • salt, work factor, migration 전략이 있는가?
  • secret은 코드, image, log, CI output에 남아 있지 않은가?
  • key는 암호화용인가, 서명용인가?
  • TLS 인증서와 private key는 자동 갱신과 폐기 확인이 되는가?

확인할 증거

  • password hash에 알고리즘 id와 work factor가 들어 있는지 본다.
  • legacy hash를 로그인 시점에 재해시하는지 본다.
  • secret inventory와 rotation history가 있는지 본다.
  • secret read와 rotation event가 감사 로그로 남는지 본다.
  • TLS 인증서 만료, SAN, private key 접근 권한을 확인한다.

진행 순서

  1. password hashing, secret storage, encryption, signature, TLS를 분리한다.
  2. password verifier는 Argon2, bcrypt, PBKDF2 같은 password hashing 정책으로 다룬다.
  3. secret은 저장 위치보다 접근, 감사, rotation, 폐기를 기준으로 본다.
  4. 암호화와 서명은 얻고 싶은 보안 속성을 먼저 정한다.
  5. TLS와 key lifecycle은 운영 자동화와 만료 알림까지 포함한다.

찾아볼 문서

흔한 실수

  • 비밀번호를 암호화해서 저장하려 한다.
  • secret을 환경 변수에 넣었다는 이유로 lifecycle 관리가 끝났다고 본다.
  • JWT signing key와 encryption key를 같은 rotation 기준으로 둔다.
  • TLS 인증서 만료 알림만 있고 갱신 실패 대응이 없다.

정리

비밀번호, secret, key는 모두 민감하지만 같은 문제가 아니다. 얻고 싶은 보안 속성에 따라 저장, 검증, 회전, 폐기 방식이 달라진다.

가져갈 한 문장

비밀값을 다룰 때는 “숨겼는가”보다 “어떻게 검증하고 어떻게 바꿀 수 있는가”를 묻는다.

Guide 10. 사고 대응은 로그를 읽는 일이 아니라 순서를 복원하는 일이다

상황

새벽에 세 가지 알림이 이어졌다.

refresh token 재사용, 특정 계정의 결제 시도 증가, 관리자 권한 변경 실패가 같은 시간대에 발생했다.

각 로그를 따로 보면 작은 이상 징후지만, 순서를 연결하면 계정 탈취와 권한 우회 시도일 수 있다.

먼저 판단할 것

  • 같은 actor, account, device, trace id, incident id로 묶이는 이벤트가 있는가?
  • 감사 로그와 security event가 같은 목적의 로그로 섞여 있지 않은가?
  • 민감값 없이도 영향 범위를 복원할 수 있는가?
  • containment 전에 증거 snapshot을 만들었는가?
  • 사고 보고서에 남은 불확실성이 기록되는가?

확인할 증거

  • login, token, permission, payment, support action 로그를 시간순으로 정렬한다.
  • actor와 subject가 분리되어 있는지 본다.
  • token 원문, password, 주민번호, 카드번호가 로그에 없는지 본다.
  • rate limit과 abuse detection rule이 어떤 key와 window에서 매칭되었는지 본다.
  • 사고 대응 조치가 incident id로 남는지 본다.

진행 순서

  1. 원본 로그와 snapshot을 보존한다.
  2. 이벤트를 시간순으로 묶는다.
  3. 허용 이벤트와 거부 이벤트를 모두 본다.
  4. 영향 계정, token, resource, action 범위를 산정한다.
  5. containment를 적용하고 그 조치도 감사 로그로 남긴다.
  6. 보고서에 원인, 영향, 조치, 남은 불확실성, 재발 방지 owner를 남긴다.

찾아볼 문서

흔한 실수

  • 성공 로그만 보고 거부 이벤트를 무시한다.
  • 로그에 민감값을 많이 남기면 사고 분석이 쉬워진다고 생각한다.
  • 증거 보존 전에 서버 재시작과 로그 삭제를 한다.
  • 사고 보고서를 “수정 완료” 한 줄로 끝낸다.

정리

보안 로그의 목적은 나중에 믿고 검색할 수 있는 증거를 남기는 것이다. 사고 대응은 그 증거로 순서를 복원하는 작업이다.

가져갈 한 문장

사고 대응에서 가장 먼저 지켜야 할 것은 서비스만이 아니라 증거다.

Guide 11. 구현 전에 체크리스트를 문서가 아니라 작업 순서로 쓴다

상황

신규 주문 취소 API를 구현해야 한다.

설계 문서도 있고, 보안 체크리스트도 있다.

하지만 체크리스트가 마지막 PR 템플릿에만 붙어 있으면 이미 잘못된 구조가 굳어진 뒤에야 문제를 발견한다.

먼저 판단할 것

  • 설계 단계에서 actor, resource, action, owner check가 정의되었는가?
  • 구현 중 Controller, Service, Repository 경계가 각각 살아 있는가?
  • 테스트는 실패해야 하는 요청을 포함하는가?
  • 배포 전 운영 설정과 rollback 경로까지 확인하는가?
  • 리뷰 질문이 실제 diff에 붙어 있는가?

확인할 증거

  • API 설계서에 보안 요구사항이 명시되어 있는지 본다.
  • DTO, policy, repository method, audit event가 같은 action 이름을 쓰는지 본다.
  • 테스트에 인증 없음, 권한 없음, 비소유자, 예상 밖 필드, 반복 호출이 있는지 본다.
  • 배포 설정에서 CORS, secret, TLS, logging, alert, rollback을 본다.
  • 리뷰 코멘트가 재사용 가능한 테스트나 체크리스트 항목으로 남는지 본다.

진행 순서

  1. 설계 전 체크리스트로 빠진 보안 질문을 찾는다.
  2. 구현 중 체크리스트로 코드 경계를 확인한다.
  3. 테스트 체크리스트로 실패 요청과 로그를 검증한다.
  4. 배포 전 체크리스트로 운영 설정을 확인한다.
  5. 코드 리뷰 질문으로 diff를 공격 요청처럼 읽는다.

찾아볼 문서

흔한 실수

  • 체크리스트를 PR 마지막에 형식적으로 붙인다.
  • 구현 체크와 운영 체크를 분리하지 않는다.
  • 테스트가 happy path만 확인한다.
  • 리뷰에서 발견한 보안 문제를 재사용 가능한 항목으로 남기지 않는다.

정리

체크리스트는 문서가 아니라 작업 순서다. 설계, 구현, 테스트, 배포, 리뷰 단계에서 각각 다른 질문을 해야 한다.

가져갈 한 문장

좋은 체크리스트는 “확인했습니다”가 아니라 “이 코드와 이 테스트가 증거입니다”로 끝난다.

Guide 12. 코드 리뷰에서는 diff를 공격 요청처럼 읽는다

상황

PR에 검색 API 개선, 관리자 권한 변경 API, OAuth callback 수정이 함께 들어왔다.

리뷰어가 스타일과 성능만 보면 보안 경계 변경을 놓칠 수 있다.

이 PR에서 공격자가 바꿀 수 있는 값과 실패 시 남는 증거를 따라가야 한다.

먼저 판단할 것

  • 새로 열린 entry point가 있는가?
  • client 입력이 보안 결정에 바로 쓰이는가?
  • 권한 검사가 Controller, Service, Repository 어디에 있는가?
  • 오류 응답이 내부 정보를 드러내는가?
  • 테스트와 로그가 실패 경로를 증명하는가?

확인할 증거

  • 변경된 Controller의 path, method, request body를 본다.
  • DTO에 민감 필드가 들어왔는지 본다.
  • 동적 query에서 값과 구조가 분리되었는지 본다.
  • @PreAuthorize, policy service, repository condition을 따라간다.
  • OAuth callback에서 state, PKCE, redirect URI 검증이 유지되는지 본다.
  • 실패 응답과 security event 이름을 확인한다.

진행 순서

  1. diff에서 새 입력면을 찾는다.
  2. 입력값 중 공격자가 바꿀 수 있는 값을 표시한다.
  3. 보안 결정이 서버 데이터와 principal 기준으로 내려지는지 본다.
  4. 실패 응답과 로그를 확인한다.
  5. 테스트가 우회 요청을 포함하는지 본다.
  6. 리뷰 코멘트를 체크리스트나 테스트로 남긴다.

찾아볼 문서

흔한 실수

  • 보안 리뷰를 별도 보안팀만 하는 일로 미룬다.
  • 권한 annotation만 보고 데이터 조회 조건을 보지 않는다.
  • 테스트가 성공 케이스만 있어서 리뷰 코멘트를 증명하지 못한다.
  • OAuth callback 수정에서 state 저장소와 redirect URI 검증을 놓친다.

정리

보안 코드 리뷰는 “이 코드가 예쁜가”가 아니라 “공격자가 어떤 값을 바꾸면 어디서 막히는가”를 확인하는 일이다.

가져갈 한 문장

리뷰할 때는 diff를 기능 변경이 아니라 새 공격면으로 읽는다.

Guide 13. 배포 전에는 코드보다 운영 경계를 다시 본다

상황

보안 테스트는 통과했고 PR도 승인됐다.

하지만 production 배포 직전, gateway CORS 설정과 application CORS 설정이 다르고, secret은 CI log에 일부 출력되며, rollback manifest에는 이전 TLS 설정이 남아 있다.

코드가 안전해도 운영 경계가 틀리면 배포 순간 취약점이 된다.

먼저 판단할 것

  • production domain에서 HTTPS, HSTS, Secure cookie가 실제로 적용되는가?
  • gateway, WAF, Spring Security matcher가 같은 보호 기준을 가지는가?
  • secret은 build가 아니라 runtime에 주입되는가?
  • alert와 log retention이 사고 대응에 충분한가?
  • rollback하면 오래된 insecure 설정이 되살아나지 않는가?

확인할 증거

  • 배포 manifest, gateway route, WAF rule, Spring 설정을 비교한다.
  • CORS allowlist와 credential 포함 여부를 production domain 기준으로 본다.
  • secret scan 결과와 CI/CD log masking 상태를 확인한다.
  • TLS 인증서 만료와 key 접근 권한을 확인한다.
  • rate limit, abuse detection, alert rule이 실제 endpoint에 연결되는지 본다.

진행 순서

  1. production에서 실제 적용될 설정을 기준으로 본다.
  2. gateway와 애플리케이션의 보안 경계를 비교한다.
  3. secret과 key가 image, log, env dump에 남지 않는지 본다.
  4. 보안 header와 cookie 속성을 실제 응답으로 확인한다.
  5. alert와 rollback이 보안 경계를 깨지 않는지 본다.
  6. 배포 후 smoke test에 인증, 인가, CORS, 로그 확인을 포함한다.

찾아볼 문서

흔한 실수

  • staging 설정이 안전하니 production도 같을 것이라고 가정한다.
  • rollback manifest의 오래된 CORS나 key 설정을 확인하지 않는다.
  • secret이 CI log에 찍히지 않는지만 보고 image layer를 확인하지 않는다.
  • WAF가 있으니 애플리케이션 권한 검사를 느슨하게 둔다.

정리

배포 전 보안은 코드 리뷰의 반복이 아니다. 실제 운영 경계, secret, header, 로그, rollback을 확인하는 별도 단계다.

가져갈 한 문장

보안 배포는 “코드가 맞다”가 아니라 “production에서 그대로 맞다”를 확인하는 일이다.

Guide 14. 학습을 끝낼 때는 문서를 테스트와 runbook으로 바꾼다

상황

Web Security 문서를 모두 읽었다.

읽을 때는 이해한 것 같지만, 실제 PR이나 사고 상황이 오면 어느 문서를 다시 열어야 할지 막힌다.

학습을 실력으로 바꾸려면 문서 제목을 외우는 대신 자신의 작업 루틴으로 바꿔야 한다.

먼저 판단할 것

  • 내가 만든 API에 대해 요청 경계 그림을 그릴 수 있는가?
  • 인증, 인가, 브라우저 보안, 입력 검증, 로그를 한 요청에서 연결해 설명할 수 있는가?
  • 각 문서에서 테스트로 바꿀 수 있는 질문을 뽑았는가?
  • 사고 대응 시 어떤 로그를 먼저 볼지 정해 두었는가?
  • 내 프로젝트의 보안 기본선과 운영 확장선을 나누었는가?

확인할 증거

  • 내 API 설계서에 actor, resource, action, trust boundary가 있는지 본다.
  • 테스트 코드에 인증 없음, 권한 없음, 비소유자, 예상 밖 필드, 잘못된 Origin, 반복 호출이 있는지 본다.
  • 로그에 actor, resource, action, result, reason, trace id가 있는지 본다.
  • 사고 대응 runbook에 증거 보존, containment, 사용자 보호, 보고서 작성 순서가 있는지 본다.
  • 학습 노트가 “설정 이름”보다 “막는 공격”을 중심으로 정리되어 있는지 본다.

진행 순서

  1. 전체 지도 문서를 다시 열어 요청 흐름을 그린다.
  2. 각 장의 압축 정리에서 핵심 질문을 뽑는다.
  3. 상세 문서에서 취약 요청과 개선 요청을 하나씩 골라 내 프로젝트에 맞게 변형한다.
  4. API 보안 체크리스트를 PR 템플릿이 아니라 작업 순서로 바꾼다.
  5. 실전 플레이북을 내 서비스의 사고 대응 runbook으로 줄인다.
  6. 다음 PR에서 이 가이드북의 Guide 하나를 실제로 적용해 본다.

찾아볼 문서

흔한 실수

  • 문서를 읽은 순서와 실무에서 판단하는 순서를 같다고 생각한다.
  • Spring Security 설정 이름은 외웠지만 어떤 공격을 막는지 설명하지 못한다.
  • 체크리스트를 실제 테스트와 로그로 바꾸지 않는다.
  • 사고 대응 문서를 읽고도 내 서비스의 로그 필드가 충분한지 확인하지 않는다.

정리

학습의 끝은 문서를 덮는 순간이 아니라, 다음 설계와 PR과 사고 대응에서 다시 열 수 있는 루틴을 만드는 순간이다.

가져갈 한 문장

보안 문서는 읽어서 끝나는 것이 아니라 테스트와 로그와 runbook으로 바뀔 때 실력이 된다.