방어자 관점
🔐 애플리케이션 보안 학습 로드맵
공격을 배우는 로드맵이 아니라, 내가 만든 앱이 무너지지 않게 만드는 순서입니다.
왜 이 분야만 유독 시작이 막히는가
보안을 공부하려고 검색하면 대부분의 자료가 공격하는 쪽에서 쓰여 있습니다. 취약점을 찾아내고 뚫는 과정을 따라가는 자료들인데, 그건 별개의 직무이고 이 문서의 주제가 아닙니다. 여기서 다루는 것은 자기 애플리케이션을 만드는 개발자가 스스로 지키는 방법입니다. 그래서 이 로드맵에는 공격 절차, 페이로드, 침투 도구 사용법이 들어 있지 않습니다. 대신 어떤 종류의 결함이 존재하는지, 그 결함이 왜 생기는지, 코드에서 무엇을 바꾸면 그 부류 전체가 사라지는지를 다룹니다. 결함 하나하나를 외우는 방식은 오래 못 갑니다. 결함이 생겨나는 자리를 알아보는 눈이 있어야 처음 보는 문제도 같은 방식으로 풀립니다.
또 하나 시작을 막는 이유는 보안이 별도의 챕터처럼 보인다는 점입니다. 실제로는 기능을 만드는 그 순간의 결정들이 그대로 보안 결과가 됩니다. 사용자 입력을 어떤 형태로 받을지, 로그인 상태를 어디에 어떻게 보관할지, 데이터를 조회할 때 소유자를 어디서 확인할지 같은 것들입니다. 그래서 이 로드맵은 위협 목록을 나열하는 대신 앱을 만드는 순서를 따라갑니다. 반대로 여기서 빼는 것도 분명히 해 두겠습니다. 암호 알고리즘의 수학적 내부, 네트워크 계층과 방화벽 운영, 클라우드 인프라 보안, 규제 준수 문서화, 그리고 사고 대응 조직 운영입니다. 전부 실제 업무지만 개발자가 자기 앱을 지키는 데 필요한 첫 다섯 걸음은 아닙니다. 첫 프로젝트 수준에서 챙길 최소 항목만 빠르게 훑고 싶다면 첫 프로젝트의 보안 기본기를 먼저 보고 오셔도 됩니다.
순서대로 쌓는 다섯 겹
- 신뢰 경계와 입력 검증
- 인증과 세션
- 인가와 권한
- 흔한 취약점 유형과 방어
- 비밀 관리와 의존성
이 순서를 뒤집으면 대부분 헛수고가 됩니다. 누구인지 모르는 상태에서 권한을 따질 수 없고, 어디까지가 내 통제 밖인지 모르는 상태에서 검증할 지점을 정할 수 없기 때문입니다. 아래 기간은 주당 5시간 정도를 가정했습니다. 보안은 별도의 프로젝트로 공부하기보다 이미 만들고 있는 앱에 하나씩 적용하며 익히는 편이 훨씬 빨리 붙습니다.
1단계 · 신뢰 경계 — 어디까지가 내 통제 밖인지 선 긋기
가장 먼저 익힐 것은 기술이 아니라 그림 하나입니다. 내 코드가 통제하는 영역과 그렇지 않은 영역 사이에 선을 긋고, 그 선을 넘어 들어오는 모든 것에 이름을 붙이는 작업입니다. 폼 입력만이 아닙니다. URL 경로와 쿼리 문자열, 요청 헤더, 쿠키, 업로드 파일의 이름과 내용, 외부 API의 응답, 그리고 브라우저에서 실행되는 내 코드가 보낸 값까지 전부 경계 바깥입니다. 특히 마지막 항목이 자주 잊힙니다. 프론트엔드 코드는 내가 썼지만 실행되는 곳은 사용자의 기기이고, 거기서 무엇이 보내질지는 내가 정할 수 없습니다. 그러므로 클라이언트 검증은 사용자 편의를 위한 것이고, 판단의 근거가 되는 검증은 반드시 서버에 있어야 합니다.
검증 방식에는 두 가지 접근이 있습니다. 금지할 것을 나열하는 방식과, 허용할 것만 정의하고 나머지를 전부 거절하는 방식입니다. 후자를 기본으로 삼으세요. 금지 목록은 언제나 빠뜨린 항목이 남지만, 허용 목록은 빠뜨리면 기능이 동작하지 않아 곧바로 드러납니다. 실무에서는 필드마다 타입, 길이 범위, 허용 문자, 값의 집합을 선언해 두고 그 선언을 통과한 값만 아래 계층으로 넘기는 구조를 만듭니다. 검증에 실패했을 때 무엇을 돌려줄지도 이 단계에서 정합니다. 오류 응답에 내부 파일 경로나 스택 트레이스를 담지 않는 것이 원칙입니다.
완료 기준은 자기 앱의 요청 하나를 골라 값이 들어오는 지점을 빠짐없이 나열하고, 각각이 어디에서 검증되는지 코드 위치를 짚을 수 있는 상태입니다. 체크포인트는 기존 API 하나를 골라 입력 스키마를 명시적으로 선언하고, 규격을 벗어난 요청이 처리 로직에 도달하기 전에 일관된 형식으로 거절되게 고치는 작업입니다. 자주 막히는 곳은 검증을 여러 계층에 조금씩 나눠 두는 것입니다. 컨트롤러에서 길이를 보고, 서비스에서 형식을 보고, 저장 직전에 또 무언가를 보는 식이 되면 어느 값이 이미 안전한지 아무도 모르게 되어 결국 아무도 믿지 않고 아무도 검증하지 않는 상태가 됩니다. 경계에서 한 번 검증하고, 통과한 값은 검증된 타입으로 바꿔 넘기세요.
2단계 · 인증과 세션 — 누구인지 확인하고 그 사실을 유지하기
인증은 두 부분으로 나뉩니다. 자격 증명을 확인하는 순간과, 그 확인 결과를 이후 요청에서 유지하는 방법입니다. 앞쪽에서 배울 것은 비밀번호를 절대 그대로 저장하지 않고 그 용도로 설계된 해시 함수로 변환해 저장한다는 것, 그 함수는 직접 만들지 않고 프레임워크나 검증된 라이브러리가 제공하는 것을 그대로 쓴다는 것, 그리고 로그인 실패 응답이 아이디가 틀렸는지 비밀번호가 틀렸는지 구분해 주지 않는다는 것 정도입니다. 여기에 시도 횟수 제한과 잠금 정책이 붙습니다.
뒤쪽, 세션 유지가 실제로 더 자주 사고가 나는 곳입니다. 식별자를 어디에 담을지(쿠키인지 저장소인지), 만료를 얼마로 둘지, 로그아웃했을 때 서버 쪽에서도 실제로 무효화되는지, 비밀번호를 바꾸면 다른 기기의 세션이 끊기는지 같은 결정들입니다. 쿠키를 쓴다면 HttpOnly로 스크립트 접근을 막고, Secure로 암호화된 연결에서만 전송되게 하고, SameSite로 다른 사이트에서 시작된 요청에 딸려 가는 범위를 제한하는 것이 기본 세트입니다. 토큰을 쓰기로 했다면 "서버에 상태를 두지 않는다"는 장점이 곧 "발급된 토큰을 즉시 취소하기 어렵다"는 비용과 한 쌍이라는 점을 이해하고 선택해야 합니다.
SESSION COOKIE — 기본으로 켜 두는 속성
Set-Cookie: sid=<server-generated-random-id>;
HttpOnly; # 페이지 스크립트가 읽지 못하게
Secure; # HTTPS 연결에서만 전송
SameSite=Lax; # 외부 사이트에서 시작된 요청 제한
Path=/;
Max-Age=1209600 # 만료를 명시. 무기한 금지
# 로그아웃은 쿠키를 지우는 것으로 끝나지 않습니다.
# 서버에 저장된 세션 레코드를 함께 무효화해야 합니다.완료 기준은 로그인부터 로그아웃까지 어떤 값이 어디에 저장되고 언제 사라지는지 그림으로 그릴 수 있고, 비밀번호 변경 시 기존 세션이 어떻게 되는지 설명할 수 있는 상태입니다. 체크포인트는 자기 앱에 "로그인된 기기 목록" 화면을 붙이고, 개별 세션을 끊는 기능을 만드는 것입니다. 이 기능을 만들려면 세션을 서버가 실제로 관리하고 있어야 해서, 구조가 허술하면 곧바로 드러납니다. 자주 막히는 곳은 비밀번호 재설정 흐름입니다. 재설정 링크의 토큰을 예측 가능한 값으로 만들거나, 만료를 두지 않거나, 한 번 쓰고 폐기하지 않거나, 재설정 후에도 기존 세션을 그대로 두는 실수가 반복됩니다. 재설정 토큰은 충분한 무작위성으로 생성하고, 짧은 만료를 두고, 사용 즉시 폐기하고, 성공하면 해당 계정의 다른 세션을 모두 끊으세요.
3단계 · 인가 — 이 사람이 이 자원에 손댈 수 있는가
인증이 끝났다고 보안이 끝나는 게 아닙니다. 실제 사고의 상당수는 로그인한 정상 사용자가 남의 자원을 건드릴 수 있는 상태에서 벌어집니다. 요청 경로의 식별자만 바꿔서 다른 사용자의 문서를 조회하거나, 화면에는 보이지 않는 관리자 기능을 주소로 직접 부르는 식입니다. 원인은 대부분 같습니다. 권한 확인이 화면 렌더링 쪽에만 있고 데이터 접근 경로에는 없는 것입니다. 버튼을 숨기는 것은 인가가 아닙니다.
배울 것은 권한 모델을 정하는 방법과 그것을 강제하는 위치입니다. 역할 기반으로 갈지 자원 소유권 기반으로 갈지, 둘을 섞는다면 어느 쪽이 우선인지 먼저 문장으로 적습니다. 그다음이 더 중요합니다. 그 규칙을 각 핸들러에 흩어 두지 말고 데이터를 꺼내는 지점 하나에서 강제하는 구조를 만드는 것입니다. 조회 조건 자체에 소유자 제약을 넣어 애초에 남의 행이 나오지 않게 하는 방식이 대표적입니다. 그리고 기본값은 거부여야 합니다. 새 엔드포인트를 추가했을 때 아무 선언도 하지 않으면 아무도 접근하지 못하는 쪽이, 실수로 열리는 쪽보다 훨씬 낫습니다.
완료 기준은 자기 앱의 모든 엔드포인트를 표로 적고 각 줄에 "누가"와 "어떤 조건에서"를 채울 수 있으며, 그 조건이 코드 어디에서 강제되는지 한 곳을 가리킬 수 있는 상태입니다. 체크포인트는 사용자 두 명 분량의 데이터를 만들어 두고, 한쪽 계정으로 로그인한 상태에서 다른 쪽 자원의 식별자를 넣은 요청이 전부 거부되는지 확인하는 자동 테스트를 작성하는 것입니다. 목록·상세·수정·삭제를 빠짐없이 넣으세요. 자주 막히는 곳은 수정과 삭제만 챙기고 조회를 빠뜨리는 것입니다. 특히 목록 API에 검색이나 필터 파라미터가 추가되면서 소유자 조건이 조건절 조합 과정에서 슬그머니 빠지는 경우가 많습니다. 조건을 상황마다 붙이는 대신, 소유자 제약이 이미 걸린 조회 함수를 거치지 않고는 데이터에 닿을 수 없게 만드는 편이 안전합니다.
4단계 · 흔한 결함 유형 — 부류를 알아보고 부류째로 막기
여기서 다루는 것은 개별 사례가 아니라 반복되는 네 가지 부류입니다. 첫째는 주입입니다. 데이터로 들어온 값이 명령의 일부로 해석되면서 생기는 문제이고, 데이터베이스 질의뿐 아니라 셸 명령, 파일 경로 조합, 템플릿 처리에서도 같은 형태로 나타납니다. 방어는 하나로 수렴합니다. 값을 문자열로 이어 붙여 명령을 만들지 말고, 명령의 구조와 값을 분리해 전달하는 인터페이스를 쓰는 것입니다. 값을 걸러내는 방식은 우회 경로가 계속 발견되므로 근본 처방이 아닙니다.
둘째는 XSS, 즉 사용자 입력이 페이지의 스크립트로 실행되는 부류입니다. 방어의 핵심은 출력 시점의 인코딩이며, 값이 들어가는 자리가 본문 텍스트인지 속성값인지 URL 자리인지에 따라 필요한 처리가 다릅니다. 대부분의 템플릿 엔진과 프레임워크는 기본적으로 이스케이프하므로, 위험은 그것을 일부러 끄는 지점에 집중됩니다. 사용자가 작성한 서식 있는 문서를 그대로 보여줘야 한다면 직접 필터를 짜지 말고 검증된 정화 라이브러리를 쓰고, 여기에 콘텐츠 보안 정책 헤더를 더해 실행 가능한 출처를 제한합니다.
셋째는 CSRF입니다. 로그인한 사용자의 브라우저가 다른 사이트의 유도로 내 앱에 상태 변경 요청을 보내게 되는 부류입니다. 쿠키 기반 세션에서 발생하며, 방어는 요청이 내 화면에서 시작되었음을 증명하는 값을 함께 요구하는 것과 쿠키의 SameSite 속성을 조이는 것입니다. 더불어 상태를 바꾸는 동작을 GET으로 만들지 않는 원칙이 여기서 힘을 발휘합니다. 넷째는 접근 제어 누락인데, 3단계에서 다룬 그 문제입니다. 이 부류가 유독 자주 재발하는 이유는 기능이 추가될 때마다 새 경로가 생기기 때문입니다. 그래서 코드 리뷰 체크리스트에 "이 엔드포인트의 권한 조건은 무엇이며 어디서 강제되는가"를 고정 항목으로 넣어 두는 것이 실질적인 대책입니다.
완료 기준은 자기 코드베이스를 훑으면서 네 부류 각각이 나타날 수 있는 자리를 지목하고, 현재 무엇이 그것을 막고 있는지 한 줄로 설명할 수 있는 상태입니다. 체크포인트는 앱에 댓글 기능을 추가하되, 저장 시점의 검증과 출력 시점의 인코딩을 분리해 구현하고, 상태 변경 요청 전부에 대해 CSRF 방어가 적용되는지 목록으로 확인하는 작업입니다. 자주 막히는 곳은 "프레임워크가 알아서 해 준다"는 믿음이 예외 지점에서 깨지는 것입니다. 원시 질의를 쓰는 관리자 통계 화면, 인코딩을 끄고 HTML을 그대로 넣는 공지사항 본문, 편의를 위해 CSRF 검사를 제외해 둔 몇 개의 엔드포인트가 그런 자리입니다. 프레임워크의 기본 보호를 끄는 코드는 전부 검색해서 목록으로 관리하세요.
5단계 · 비밀 관리와 의존성 — 코드 밖에서 새는 것들
마지막은 코드 로직이 아니라 코드 주변의 문제입니다. API 키, 데이터베이스 비밀번호, 서명 키 같은 값이 저장소에 커밋되거나 로그에 찍히거나 오류 화면에 노출되는 일이 대표적입니다. 원칙은 단순합니다. 비밀은 코드가 아니라 실행 환경에서 주입하고, 저장소에는 이름과 형식만 담긴 예시 파일을 둡니다. 로그를 남길 때는 요청 본문과 헤더를 통째로 찍지 말고 필요한 필드만 고르며, 인증 정보와 개인정보는 마스킹 규칙을 정해 둡니다. 그리고 실수로 커밋했다면 파일을 고치는 게 아니라 해당 값을 즉시 폐기하고 새로 발급해야 합니다. 이미 기록에 남았기 때문입니다.
의존성 쪽에서는 무엇을 쓰고 있는지 아는 것에서 출발합니다. 잠금 파일을 저장소에 포함해 설치되는 버전을 고정하고, 알려진 문제를 확인하는 점검을 정기적으로 돌리고, 결과가 나왔을 때 그 패키지가 내 앱에서 실제로 어떤 경로로 쓰이는지 판단하는 습관입니다. 여기에 더해 새 라이브러리를 넣기 전에 관리 상태를 한 번 보는 것만으로도 나중의 부담이 크게 줄어듭니다. 배포 환경에서는 오류 페이지가 상세 정보를 노출하지 않도록 설정하고, 개발용 도구나 디버그 엔드포인트가 함께 배포되지 않았는지 확인하는 것까지가 이 단계의 범위입니다.
완료 기준은 저장소를 처음 받은 사람이 비밀 값 없이도 앱을 띄우는 방법을 안내받을 수 있고, 배포 환경에 어떤 비밀이 몇 개 있으며 각각 누가 발급했는지 목록으로 답할 수 있는 상태입니다. 체크포인트는 설정을 전부 환경 변수로 옮기고, 값이 빠졌을 때 앱이 조용히 기본값으로 뜨는 대신 시작 시점에 명확한 메시지로 실패하도록 만드는 작업입니다. 여기에 의존성 점검을 자동으로 돌리는 설정을 하나 붙이면 마무리됩니다. 자주 막히는 곳은 설정을 환경 변수로 옮겨 놓고도 예전 기본값을 코드에 남겨 두는 것입니다. 환경 변수가 비면 그 기본값으로 조용히 동작해 버려서, 배포 환경이 개발용 키로 몇 달간 돌아가는 상황이 만들어집니다. 비밀 값에는 기본값을 두지 마세요.
이 로드맵의 내용은 자기 소유의 애플리케이션을 방어하기 위한 것입니다. 취약점 점검은 본인이 권한을 가진 시스템에서만 수행해야 하며, 타인의 서비스를 대상으로 하는 시험은 명시적 허가 없이는 허용되지 않습니다.
지금은 미뤄도 되는 것
보안은 범위가 넓어서 다 챙기려 들면 앱을 못 만듭니다. 첫 바퀴에서 빼도 되는 것과 그 이유를 적어 둡니다.
- 암호 알고리즘의 내부 원리. 알아 두면 좋지만, 개발자가 실무에서 내리는 결정은 "직접 구현하지 않고 검증된 구현을 올바른 용도로 쓴다"로 거의 끝납니다. 내부를 몰라서 생기는 사고보다 직접 구현해서 생기는 사고가 훨씬 많습니다.
- 위협 모델링 방법론과 문서 양식. 1단계의 신뢰 경계 그림이 그 방법론의 핵심을 이미 담고 있습니다. 팀이 커지고 합의할 상대가 생겼을 때 형식을 갖추면 됩니다.
- 인프라와 네트워크 계층 보안. 방화벽 규칙이나 네트워크 분리는 배포 형태가 정해진 뒤에 의미가 생깁니다. 관리형 플랫폼에 올린 앱 한 대라면 애플리케이션 계층에서 할 일이 아직 훨씬 많습니다.
- 침투 테스트 도구와 자격증. 공격 쪽 기술은 별도의 직무 경로이고, 방어를 목표로 한다면 여기서 얻는 것보다 앞의 다섯 단계를 자기 코드에 적용하는 쪽이 훨씬 남습니다.
사고가 이미 났다고 가정해 보기
다섯 단계를 돌고 나면 마지막으로 해 볼 연습이 하나 있습니다. 내 앱에서 사고가 이미 일어났다고 가정하고 세 가지 질문에 답해 보는 것입니다. 무슨 일이 있었는지 알아낼 기록이 남아 있는가, 피해 범위를 계정 단위로 특정할 수 있는가, 자격 증명을 전부 갈아 끼우려면 몇 시간이 걸리는가. 이 세 질문에 답하지 못한다면 아직 방어가 아니라 운에 기대고 있는 상태입니다. 세 번째 질문의 답을 짧게 만드는 작업이 특히 값어치가 큽니다. 키를 언제든 교체할 수 있게 해 두면, 의심스러운 상황에서 망설임 없이 교체할 수 있습니다.
이후 방향은 만드는 것에 달려 있습니다. 브라우저 쪽 입력과 출력 처리를 더 깊이 보고 싶다면 프론트엔드 로드맵의 4단계와 이어지고, 서버와 데이터 계층으로 넘어간다면 백엔드 로드맵이나 웹 개발 로드맵이 다음 순서입니다. 요청과 응답 구조를 다시 정리하고 싶다면 API 요청과 응답 이해하기와 HTTP 비교를, AI 기능을 붙일 계획이라면 AI 보안과 윤리를 함께 보세요.