보안 코드리뷰는 취약점 이름보다 데이터 흐름을 먼저 본다
입력값, 권한, 외부 호출, 로그를 따라가며 취약점 가능성을 찾는 코드리뷰 휴리스틱을 제시한다. Secure Code Review·Data Flow·AppSec 관점에서 기본 개념부터 구현 순서, 검증 방법, 문제가 생겼을 때 되돌리는 절차까지 설명한다.
핵심 요약
보안 코드리뷰는 취약점 이름을 대입하는 작업이 아니라 신뢰하지 않는 데이터가 어디서 들어와 어떤 변환과 권한 판단을 거쳐 위험한 동작에 도달하는지 추적하는 작업이다. source, trust boundary, authorization, sink, side effect, log를 한 줄로 연결하면 자동 도구가 놓치는 업무 로직 문제를 찾기 쉽다.
PR에서 “SQL injection이 있는가”, “XSS가 있는가”를 하나씩 묻는 방식은 알려진 패턴을 찾는 데 도움이 된다. 그러나 현대 애플리케이션의 큰 사고는 종종 취약점 이름 하나로 설명되지 않는다. 정상적으로 파싱된 사용자 입력이 다른 tenant의 객체를 가리키고, 권한 검사는 첫 요청에서만 이뤄지며, 비동기 작업이 관리자 서비스 계정으로 실행되면서 데이터가 노출되는 식이다.
OWASP Secure Code Review Cheat Sheet는 자동 도구가 놓치기 쉬운 업무 로직과 데이터 흐름을 사람이 검토해야 한다고 설명한다. 코드리뷰의 중심을 취약점 명칭에서 데이터와 권한의 이동으로 바꾸면 새로운 프레임워크와 기능에도 같은 질문을 적용할 수 있다.
다섯 가지를 먼저 표시한다
리뷰를 시작하기 전에 변경 diff에서 다음 표식을 찾는다.
- Source: HTTP body, query, header, cookie, webhook, 파일, 메시지 큐, DB 값, LLM 출력처럼 신뢰하지 않는 데이터가 들어오는 곳
- Trust boundary: 사용자에서 서버, tenant 간, 서비스 간, 외부 SaaS 간 경계
- Transformation: 파싱, 정규화, 타입 변환, decode, merge, 템플릿 렌더링
- Authorization decision: 누가 어떤 객체에 어떤 action을 할 수 있는지 판단하는 곳
- Sink/side effect: SQL, 파일, shell, URL 요청, 이메일, 결제, 권한 변경, 로그, 외부 API 호출
이 다섯 지점을 선으로 연결하면 “검증이 있느냐”보다 “올바른 데이터에 올바른 순서로 적용되느냐”를 볼 수 있다.
리뷰 순서: 입력에서 부작용까지 따라간다
1. 입력의 실제 출처를 확인한다
서버가 만든 값처럼 보여도 원래 사용자가 저장한 DB 필드일 수 있다. 메시지 큐의 payload도 생산 서비스가 침해되거나 계약이 바뀌면 신뢰할 수 없다. 각 필드에 대해 다음을 묻는다.
- 누가 이 값을 만들 수 있는가.
- 길이, 형식, 문자 집합, 허용 값은 무엇인가.
- 정규화 전후에 검증 순서가 바뀌지 않는가.
- 중복 필드, 대소문자, Unicode, URL encoding이 해석을 바꾸는가.
- 누락·null·빈 문자열에서 기본값이 더 큰 권한을 주지 않는가.
2. identity와 객체 권한을 분리한다
로그인 여부를 확인했다고 객체 접근이 허용되는 것은 아니다. 다음 코드는 사용자 인증은 했지만 invoiceId의 소유권을 확인하지 않는다.
const user = requireUser(session);
const invoice = await db.invoice.findUnique({ where: { id: req.params.invoiceId } });
return invoice;
조회 조건에 tenant 또는 owner를 포함하거나 중앙 authorization 계층에서 user, action, resource를 함께 판단해야 한다.
const user = requireUser(session);
const invoice = await db.invoice.findFirst({
where: { id: req.params.invoiceId, tenantId: user.tenantId }
});
if (!invoice) throw new NotFoundError();
리뷰에서는 read뿐 아니라 update, delete, export, share, retry, preview endpoint가 같은 정책을 사용하는지 확인한다.
3. 변환과 검증의 순서를 본다
경로를 검증한 뒤 URL decode를 하거나, allowlist 확인 후 redirect를 따라가거나, JSON schema 검증 후 다른 객체와 merge하면 위험한 값이 나중에 생길 수 있다. 원칙은 최종적으로 사용될 표현을 만든 뒤 그 표현에 정책을 적용하는 것이다.
- 파일 경로는 canonicalize 후 허용 루트 내부인지 확인
- URL은 parse 후 scheme, host, port, redirect 정책을 확인
- HTML은 출력 context에 맞게 encode
- 숫자·가격은 클라이언트 값이 아니라 서버 기준으로 계산
- role·owner·status 같은 보안 필드는 mass assignment 대상에서 제외
4. 외부 호출과 간접 입력을 추적한다
사용자가 URL을 직접 입력하지 않아도 DB에 저장된 avatar URL, webhook callback, RSS 주소, PDF 내부 링크가 서버 요청으로 이어질 수 있다. SSRF 검토에서는 최초 URL뿐 아니라 DNS 재해석, redirect, IPv6, cloud metadata, 내부 도메인 접근을 고려한다.
웹훅은 서명 검증 후에도 재전송과 중복 부작용을 막아야 한다. 관련 구현은 웹훅 서명·재전송 방지의 timestamp, event ID, idempotency 흐름과 함께 검토한다.
5. 비동기 경계에서 권한이 바뀌는지 본다
웹 요청은 일반 사용자 권한으로 시작하지만 queue worker는 서비스 관리자 권한으로 실행될 수 있다. enqueue 시점의 사용자·tenant·action을 명시적으로 전달하고 worker가 다시 검증해야 한다. “앞 단계에서 확인했으니 안전하다”는 가정은 메시지 재처리와 장기 지연에서 깨진다.
sink별 핵심 질문
| Sink | 리뷰 질문 |
|---|---|
| SQL/ORM | 동적 query, tenant 조건, raw SQL, 정렬·필드 선택이 제한되는가 |
| 파일 | 경로 정규화, 확장자보다 실제 형식, 저장 위치, 실행 권한이 안전한가 |
| shell/process | 문자열 결합 대신 인자 배열을 쓰고 실행 가능 명령을 제한하는가 |
| HTTP client | scheme·host·port·redirect·DNS·timeout·응답 크기를 제한하는가 |
| 템플릿/HTML | 출력 context에 맞는 encoding과 CSP가 있는가 |
| 로그 | token, 비밀번호, 세션, 개인정보가 남지 않으며 log injection을 막는가 |
| 메시지/웹훅 | 출처, freshness, 중복, 순서, 재시도에 안전한가 |
| 권한 변경 | 재인증, 승인, 감사 로그, 이전 값이 남는가 |
| 결제·재고 | 서버 기준 금액과 idempotency를 사용하는가 |
| LLM·도구 호출 | 모델 출력이 권한 판단이나 실행 명령으로 바로 이어지지 않는가 |
자동 도구와 사람이 나눠 볼 영역
SAST와 dependency scanner는 넓은 코드에서 알려진 패턴을 빠르게 찾는다. 사람이 더 잘 보는 영역은 다음과 같다.
- tenant와 객체 소유권
- 상태 전이와 순서 의존성
- 두 endpoint를 조합한 권한 우회
- 관리자·지원 도구의 과도한 기능
- 정상 기능을 이용한 대량 추출과 비용 남용
- 비동기 작업의 identity 손실
- 예외 처리에서의 fail-open
- 로그·분석 SDK를 통한 민감정보 유출
도구 결과는 리뷰 시작점으로 사용하고, false positive 수만 줄이는 것을 성과로 삼지 않는다. 중요한 sink에 도달하는 고신뢰 데이터 흐름을 우선 처리한다.
변경 크기에 따른 리뷰 깊이
일반 리뷰
문자열, UI, 테스트처럼 신뢰 경계가 바뀌지 않는 변경은 기본 체크로 처리한다.
강화 리뷰
다음 변경은 별도 보안 관점 검토를 요구한다.
- 인증·세션·권한 정책
- 외부 입력 형식과 parser
- 파일 업로드·다운로드
- 결제·환불·크레딧
- 관리자·지원 기능
- 웹훅·메시지·배치 작업
- 외부 URL 호출과 integration
- 암호화·키·인증서
- 모바일 token과 API 키
- LLM RAG·tool 실행
설계 리뷰
새 trust boundary, multi-tenant 구조, 고권한 자동화, 대량 데이터 이동은 diff 리뷰 전에 데이터 흐름도와 threat model을 검토한다. 코드가 완성된 뒤 구조 문제를 고치면 비용이 크게 늘어난다.
실패 모드
validation 함수가 있다는 사실만 확인한다
어떤 표현에 언제 적용되는지, 우회 가능한 다른 경로가 있는지 봐야 한다. 같은 객체를 수정하는 admin API와 batch job이 검증을 공유하지 않을 수 있다.
controller만 보고 service와 worker를 보지 않는다
권한 결정과 실제 부작용이 다른 계층에 있으면 끝까지 추적해야 한다. 특히 retry와 scheduled job이 원래 사용자의 제한을 잃기 쉽다.
테스트가 정상 성공만 검증한다
다른 tenant ID, 만료 token, 순서가 바뀐 요청, 중복 webhook, 삭제된 객체, 권한 변경 직후 세션을 포함한 음성 테스트가 필요하다.
보안 로그가 민감정보를 복제한다
디버깅을 위해 전체 request body와 token을 남기면 사고 대응 시스템 자체가 새로운 데이터 저장소가 된다. 필드 allowlist와 redaction을 코드에서 강제한다.
PR에 바로 넣을 체크리스트
- 새 입력 source와 trust boundary가 설명돼 있다.
- 최종 canonical form에 validation을 적용한다.
- 인증과 객체·action authorization을 별도로 확인한다.
- tenant 조건이 모든 read/write 경로에 적용된다.
- 고위험 필드는 mass assignment에서 제외된다.
- 외부 URL, 파일, shell, 템플릿 sink가 allowlist 기반이다.
- 비동기 worker가 identity와 권한을 다시 확인한다.
- retry와 중복 실행이 같은 부작용을 반복하지 않는다.
- 로그에 token·비밀번호·민감 payload가 남지 않는다.
- 실패 시 기본 동작이 허용이 아니라 거부다.
- 정상·비정상·권한 경계 테스트가 추가됐다.
- 배포 후 관찰할 지표와 롤백 조건이 있다.
모바일 입력과 token은 모바일 API 키·서버 검증, 인증 변경은 OAuth/OIDC 세션 설계와 연결해 검토하면 누락을 줄일 수 있다.
배포 후 확인할 신호
코드리뷰는 배포 전 판단이므로 운영 신호로 가정을 검증해야 한다.
- 403·404 비율의 갑작스러운 변화
- 한 사용자의 객체 ID 탐색과 대량 실패
- tenant 간 접근 차단 이벤트
- 외부 호출 목적지와 redirect 변화
- queue 재시도·중복 처리·DLQ 증가
- 관리자 기능의 사용량과 비정상 시간대
- redaction 실패 또는 민감 로그 탐지
- 신규 endpoint의 비용·latency·오류 급증
최종 판단
좋은 보안 코드리뷰는 취약점 사전을 많이 외운 리뷰가 아니다. 신뢰하지 않는 값이 권한 판단을 거쳐 부작용에 도달하는 전 과정을 설명하고, 그 경로에서 검증이 빠지거나 identity가 바뀌는 지점을 찾는 리뷰다. source에서 sink까지 한 줄로 추적하는 습관을 PR 템플릿과 테스트에 넣으면 프레임워크가 바뀌어도 재사용 가능한 보안 기준이 된다.
전체 댓글 0개