방화벽도, TLS도, 닫힌 포트도 웹 애플리케이션의 결함을 막지 못합니다. 80번과 443번 포트는 열려 있어야 하고, 그 문으로 들어오는 공격은 애플리케이션 자신이 막아야 합니다.
들어가며: 마지막이자 가장 넓은 전선
지금까지 우리는 인프라를 단단히 했습니다. 포트를 닫고(6장), SSH를 강화하고(7장), 통신을 암호화하고(8장), 헤더를 설정했습니다(9장). 그러나 웹 서비스를 운영한다면, 한 가지는 어쩔 수 없이 열려 있어야 합니다. 웹 그 자체 — 80번, 443번 포트입니다. 사용자가 접속해야 하니까요.
그리고 바로 이 열린 문으로, 인프라 방어를 모두 통과한 공격이 들어옵니다. 웹 애플리케이션 취약점은 방화벽이 막지 못합니다. 방화벽은 포트를 여닫을 뿐, 정당하게 열린 443번 포트로 들어오는 악성 요청과 정상 요청을 구별하지 못하기 때문입니다. TLS도 막지 못합니다. TLS는 통신을 암호화할 뿐, 그 암호화된 통신 안에 담긴 공격을 막지 않습니다. 오히려 공격도 암호화되어 안전하게 전달됩니다. 닫힌 포트도, 강한 SSH도 소용없습니다. 공격은 정상적인 웹 요청의 형태로, 정문으로 들어오기 때문입니다.
그래서 웹 애플리케이션 보안은 별도의, 그리고 가장 넓은 전선입니다. 1장에서 보았듯 자동 공격의 상당수가 웹 취약점을 노리고, 11장에서 보았듯 펜테스트 발견의 다수가 웹 애플리케이션에 있습니다. 이 전선의 방어는 인프라가 아니라 애플리케이션 자신의 책임입니다.
이 장에서는 웹 애플리케이션의 주요 취약점 부류를 다룹니다. 업계의 표준 참고 자료인 OWASP가 정리한 주요 위험들을 바탕으로 하되, 단순 나열을 넘어 각 취약점의 본질과 방어 원리를 이해하는 데 초점을 둡니다. 나아가 스캐너가 결코 잡지 못하는 비즈니스 로직 결함, 그리고 실제 프레임워크에서 터진 취약점의 교훈까지 살펴봅니다. 다시 강조하지만, 이 장은 공격 방법이 아니라 방어 원리를 다룹니다.
OWASP가 뭔가요? OWASP(Open Worldwide Application Security Project)는 웹 보안 지식을 무료로 공개하는 비영리 단체입니다. 이 장에서 계속 언급할 세 가지 자료만 알아 두면 됩니다. (1) OWASP Top 10 — 가장 위험한 웹 취약점 10가지를 추린 목록. 어디서부터 봐야 할지 모를 때 출발점입니다. (2) OWASP ASVS(애플리케이션 보안 검증 표준) — "우리 앱이 안전한가"를 항목별로 체크할 수 있는 점검표. (3) OWASP Cheat Sheet Series — 취약점별로 "그래서 코드를 어떻게 짜야 하나"를 짧게 정리한 실무 치트시트. 개발자라면 북마크해 두십시오. 이 장의 각 절은 결국 Top 10의 한 항목씩을 풀어 쓴 것입니다.
12.1 인젝션: 데이터를 명령으로 착각하게 만들기
웹 취약점의 고전이자 여전히 가장 위험한 부류 중 하나가 인젝션(injection)입니다. 그 본질을 이해하면 다양한 변종을 한꺼번에 이해할 수 있습니다.
인젝션의 본질
인젝션의 핵심 발상은 이것입니다. 애플리케이션이 사용자가 입력한 데이터를, 데이터가 아니라 명령의 일부로 잘못 해석하게 만드는 것입니다. 사용자의 입력이 시스템의 명령(데이터베이스 질의, 시스템 명령 등)에 섞여 들어갈 때, 그 입력이 명령의 구조 자체를 바꿀 수 있다면, 공격자는 입력을 조작하여 의도하지 않은 명령을 실행시킬 수 있습니다.
가장 잘 알려진 예가 SQL 인젝션입니다. 애플리케이션이 사용자 입력을 데이터베이스 질의문에 그대로 끼워 넣으면, 공격자는 입력에 질의문의 구조를 바꾸는 내용을 넣어, 의도하지 않은 데이터를 빼내거나 조작할 수 있습니다. 명령 인젝션은 같은 원리가 시스템 명령에 적용된 것으로, 사용자 입력이 시스템 명령에 섞여 임의의 명령이 실행되는 것입니다.
방어 원리: 데이터와 명령의 분리
인젝션에 대한 근본적 방어는, 데이터와 명령을 엄격히 분리하는 것입니다. 사용자 입력은 언제나 데이터로만 취급되어야 하고, 결코 명령의 구조에 영향을 주어서는 안 됩니다.
데이터베이스의 경우, 이것을 달성하는 표준 방법이 매개변수화된 질의(parameterized query) 또는 준비된 문(prepared statement)입니다. 쉽게 말해, 질의문의 빈칸에 사용자 입력을 "값"으로만 꽂아 넣는 방식입니다. 이 방식에서는 질의문의 구조를 먼저 고정해 두고, 사용자 입력을 그 구조의 정해진 자리에 데이터로만 채워 넣습니다. 입력이 아무리 교묘해도 질의문의 구조를 바꿀 수 없으므로, SQL 인젝션이 원리적으로 차단됩니다. 현대의 프레임워크와 데이터베이스 라이브러리는 대부분 이 방식을 기본으로 제공하므로, 그것을 올바르게 사용하는 것이 핵심입니다. 위험한 것은, 편의를 위해 또는 무지로 인해 사용자 입력을 질의문에 직접 문자열로 이어 붙이는 것입니다.
그래서 지금 뭘 하면 되나. 코드에서 SQL 문자열을
+나 템플릿 문법으로 사용자 입력과 이어 붙이는 곳을 찾으십시오. 거의 모든 SQL 인젝션은 바로 거기서 납니다. 내가 남의 코드를 넘겨받아 점검할 때도 가장 먼저 보는 곳이 이 "문자열로 이어 붙인 쿼리"입니다 — ORM이나 쿼리 빌더를 쓰다가 한두 군데만 급하게 raw 쿼리로 박아 넣은 자리가 꼭 있습니다. 언어·프레임워크별 정확한 작성법은 OWASP SQL Injection Prevention Cheat Sheet에 정리돼 있습니다.
시스템 명령의 경우, 가능하면 사용자 입력을 시스템 명령에 전달하는 것 자체를 피하고, 불가피하다면 입력을 엄격히 검증하고 명령과 분리하는 안전한 방식을 사용해야 합니다.
입력 검증: 보조 방어선
데이터와 명령의 분리가 1차 방어라면, 입력 검증(input validation)은 보조 방어선입니다. 사용자 입력이 예상된 형식과 범위에 맞는지를 확인하여, 비정상적인 입력을 거부하는 것입니다. 예상되는 것만 허용하는(allowlist) 방식이, 알려진 나쁜 것만 막는(denylist) 방식보다 안전합니다. 다만 입력 검증은 인젝션 방어의 주된 수단이 아니라 보조 수단임을 기억하십시오. 검증만으로 인젝션을 완전히 막으려는 것은 위험하며, 반드시 데이터와 명령의 분리가 우선되어야 합니다.
12.2 XSS와 출력의 문제: 다른 사용자의 브라우저를 노리다
9장에서 CSP를 다루며 잠깐 언급한 교차 사이트 스크립팅(XSS)을 본격적으로 살펴봅니다.
XSS의 본질
인젝션이 서버 측 명령을 노린다면, XSS는 다른 사용자의 브라우저를 노립니다. 핵심 발상은, 공격자가 웹 페이지에 악성 스크립트를 주입하고, 그것이 다른 사용자의 브라우저에서 실행되게 만드는 것입니다.
예를 들어, 게시판에 글을 쓸 수 있는 사이트에서, 공격자가 글 내용에 스크립트를 심습니다. 사이트가 이 내용을 다른 사용자에게 보여 줄 때 스크립트를 걸러내지 않으면, 그 글을 보는 모든 사용자의 브라우저에서 공격자의 스크립트가 실행됩니다. 이를 통해 공격자는 사용자의 세션을 탈취하거나, 페이지를 변조하거나, 사용자의 행동을 조작하거나, 정보를 빼낼 수 있습니다.
XSS의 본질은 인젝션과 닮았습니다. 사용자(피해자)의 브라우저가, 공격자가 주입한 데이터를 실행 가능한 코드로 해석하는 것입니다. 데이터가 코드로 착각되는 같은 패턴입니다.
방어 원리: 출력 인코딩과 맥락
XSS에 대한 근본적 방어는 출력 인코딩(output encoding)입니다. 사용자가 입력한 데이터를 웹 페이지에 출력할 때, 그것이 코드로 해석되지 않고 단순한 텍스트로 표시되도록 적절히 변환하는 것입니다. 스크립트로 해석될 수 있는 특수 문자들을, 화면에는 그대로 보이되 브라우저가 코드로 실행하지는 않는 형태로 변환합니다. 이렇게 하면 공격자가 스크립트를 주입해도, 그것이 실행되지 않고 그저 텍스트로 표시됩니다.
중요한 것은 맥락(context)입니다. 데이터가 출력되는 위치 — HTML 본문인지, 속성값인지, 스크립트 안인지, URL인지 — 에 따라 필요한 인코딩이 다릅니다. 현대의 프레임워크는 대부분 맥락에 맞는 출력 인코딩을 기본으로 제공하므로, 그것을 우회하지 않고 올바르게 사용하는 것이 핵심입니다. 위험한 것은, 프레임워크의 자동 인코딩을 의도적으로 끄거나, 원시 HTML을 직접 삽입하는 기능을 부주의하게 쓰는 것입니다. 구체적으로는 React의 dangerouslySetInnerHTML, Vue의 v-html, 그리고 Laravel Blade에서 {{ }}(자동 이스케이프) 대신 쓰는 {!! !!} 같은 것들입니다. 이름에 "dangerous"가 붙은 데는 이유가 있습니다 — 이런 자리에 사용자 입력이 들어가면 자동 방어가 통째로 풀립니다. 맥락별 인코딩 규칙과, 정말 HTML을 그대로 받아야 할 때 쓰는 살균(sanitization) 방법은 OWASP Cross Site Scripting Prevention Cheat Sheet에 정리돼 있습니다.
깊이 방어로서의 CSP
9장에서 다룬 CSP가 여기서 다시 등장합니다. 출력 인코딩이 1차 방어라면, CSP는 그것이 실패했을 때를 대비한 2차 방어선입니다. 설령 어떤 경로로 스크립트 주입에 성공하더라도, CSP가 허용되지 않은 출처의 스크립트 실행을 막으면 공격이 무력화됩니다. 출력 인코딩과 CSP를 함께 쓰는 것이 깊이 방어(원칙 3)의 좋은 예입니다.
12.3 인증과 접근 제어: 누구인가와 무엇을 할 수 있는가
11장에서 잠깐 다룬 인증과 인가의 문제를 깊이 살펴봅니다. 이것은 웹 취약점 중 영향이 가장 큰 부류에 속합니다.
인증과 인가는 다르다
먼저 두 개념을 명확히 구분해야 합니다. 인증(authentication) 은 "당신이 누구인가"를 확인하는 것입니다. 로그인이 인증입니다. 인가(authorization) 는 "당신이 이것을 할 수 있는가"를 확인하는 것입니다. 특정 자원에 대한 접근 권한 확인이 인가입니다.
많은 취약점이 이 둘을 혼동하거나, 인증은 했지만 인가를 빠뜨리는 데서 발생합니다. 사용자가 로그인은 했지만(인증됨), 그 사용자가 접근하려는 특정 자원에 대한 권한이 있는지(인가)를 확인하지 않으면, 한 사용자가 다른 사용자의 자원에 접근할 수 있게 됩니다.
접근 제어 붕괴
11.5에서 언급한 안전하지 않은 직접 객체 참조(IDOR, Insecure Direct Object Reference)가 이 부류의 대표입니다. 예컨대 /order/1001에서 주소창의 숫자만 1002로 바꿨더니 남의 주문서가 그대로 뜨는 경우입니다. 식별자만 바꾸면 다른 사람의 자원이 보이는 것이죠. 이것은 인가 확인이 누락되었기 때문입니다. 시스템은 "이 사용자가 로그인했는가"만 확인하고, "이 사용자가 이 특정 자원에 접근할 권한이 있는가"는 확인하지 않은 것입니다. 우습게 들리지만, 현장에서 내가 가장 자주 마주치는 실제 결함이 바로 이 IDOR입니다. 화려한 공격 기법이 아니라, 그냥 서버에서 소유권 한 줄을 확인하지 않은 자리입니다.
더 심각한 경우는 권한 수준의 붕괴입니다. 일반 사용자가 관리자 기능에 접근할 수 있거나, 권한을 임의로 상승시킬 수 있는 경우입니다. 이것은 11.1의 권한 상승과 직결됩니다. 멀리 갈 것도 없습니다. 협업 도구 Confluence에서 터진 CVE-2023-22515(2023년)가 바로 권한 상승 결함으로, 인증되지 않은 외부인이 관리자 계정을 만들 수 있었습니다. 거대한 상용 제품조차 이렇게 뚫립니다. 접근 제어 붕괴(Broken Access Control)는 OWASP Top 10에서 수년째 1위를 지키는, 가장 흔하고 영향이 큰 부류라는 점을 기억하십시오.
방어 원리: 모든 접근에 인가 확인을
접근 제어의 근본 원리는, 모든 자원 접근에 대해, 매번, 서버 측에서 인가를 확인하는 것입니다.
몇 가지 핵심을 강조합니다. 첫째, 인가 확인은 서버 측에서 이루어져야 합니다. 클라이언트 측(브라우저)에서 메뉴를 숨기거나 버튼을 비활성화하는 것은 인가가 아닙니다. 공격자는 클라이언트 측 제약을 우회하고 서버에 직접 요청을 보낼 수 있기 때문입니다. 진짜 통제는 서버에 있어야 합니다.
둘째, 인가는 기본적으로 거부(deny by default) 여야 합니다. 명시적으로 허용된 것만 허용하고, 나머지는 모두 거부하는 것이 안전합니다. 반대로 기본 허용에 예외만 막으면, 빠뜨린 곳이 그대로 구멍이 됩니다.
셋째, 인가는 최소 권한 원칙을 따라야 합니다(7장, 11장). 각 사용자는 자신의 역할에 필요한 최소한의 접근 권한만 가져야 합니다.
세션과 인증의 안전
인증 자체의 안전도 중요합니다. 로그인 후 사용자를 식별하는 세션이 안전하게 관리되어야 합니다. 세션 식별자가 예측 가능하거나, 탈취되기 쉽거나, 적절히 만료되지 않으면, 공격자가 다른 사용자의 세션을 가로챌 수 있습니다. 또한 3장과 7장에서 강조한 다단계 인증(MFA)이 인증의 안전을 크게 높입니다. 비밀번호가 유출되어도 두 번째 요소가 막아 주기 때문입니다.
12.4 SSRF와 클라우드 메타데이터: 서버를 속여 내부를 공격하기
비교적 현대적이지만 매우 위험한 취약점으로 SSRF(Server-Side Request Forgery, 서버 측 요청 위조)가 있습니다. 클라우드 환경에서 특히 치명적입니다.
SSRF의 본질
많은 웹 애플리케이션은 사용자의 요청에 따라 서버가 다른 곳으로 요청을 보내는 기능을 가집니다. 예를 들어 사용자가 입력한 URL의 내용을 가져오거나, 외부 서비스를 호출하는 기능입니다. SSRF는 이 기능을 악용하여, 공격자가 서버로 하여금 의도하지 않은 곳으로 요청을 보내게 만드는 것입니다.
왜 위험할까요? 서버는 종종 외부 사용자가 직접 접근할 수 없는 내부 자원에 접근할 수 있기 때문입니다. 공격자가 서버를 속여 내부 네트워크의 시스템, 내부 전용 서비스, 또는 클라우드 환경의 민감한 내부 주소로 요청을 보내게 하면, 외부에서는 닿을 수 없던 내부를 서버를 통해 간접적으로 공격할 수 있습니다. 6장에서 인터넷을 향한 포트를 다 닫았어도, SSRF는 서버 자신을 발판으로 내부를 노리므로 그 방어를 우회합니다.
클라우드 메타데이터의 위험
SSRF가 클라우드 환경에서 특히 치명적인 이유가 있습니다. 많은 클라우드 환경에는 인스턴스가 자기 자신의 정보를 조회할 수 있는 내부 전용 메타데이터 주소가 있습니다. 이 메타데이터에는 때때로 그 인스턴스의 자격 증명 — 클라우드 자원에 접근할 수 있는 강력한 열쇠 — 이 포함됩니다. 공격자가 SSRF로 서버를 속여 이 메타데이터 주소에 접근하게 하면, 인스턴스의 클라우드 자격 증명을 탈취할 수 있고, 그것으로 클라우드 환경 전체를 장악할 수 있습니다. 작은 SSRF 하나가 클라우드 전체의 침해로 이어지는 것입니다.
방어 원리
SSRF에 대한 방어는 여러 겹입니다. 첫째, 가능하면 사용자 입력으로 서버가 임의의 주소에 요청을 보내는 기능 자체를 피하거나 제한합니다. 둘째, 불가피하다면 서버가 요청을 보낼 수 있는 대상을 엄격히 제한합니다(허용 목록 방식). 내부 주소나 메타데이터 주소로의 요청을 차단합니다. 셋째, 클라우드 환경에서는 메타데이터 접근에 대한 더 강화된 보호 방식을 사용하고, 인스턴스에 부여되는 권한을 최소화하여(최소 권한), 설령 자격 증명이 탈취되어도 피해를 제한합니다. 이것은 5장에서 다룬 클라우드 설정 점검과도 연결됩니다.
그래서 지금 뭘 하면 되나. "URL을 입력하면 미리보기를 보여 준다", "외부 이미지 주소를 가져와 저장한다" 같은 기능이 있다면 SSRF 후보입니다. 그 기능이 보낼 수 있는 주소를 꼭 필요한 외부 도메인으로만 좁히고(허용 목록), 사설망 대역과 클라우드 메타데이터 주소로는 절대 나가지 못하게 막으십시오. AWS를 쓴다면 인스턴스 메타데이터를 IMDSv2로 전환하는 것이 메타데이터 탈취형 SSRF에 대한 표준 완화책입니다. 검증 우회 기법까지 포함한 방어 체크리스트는 OWASP Server Side Request Forgery Prevention Cheat Sheet를 참고하십시오.
12.5 역직렬화와 기타 위험: 신뢰할 수 없는 데이터의 위험
안전하지 않은 역직렬화
다소 기술적이지만 중요한 취약점으로 안전하지 않은 역직렬화(insecure deserialization)가 있습니다. 직렬화는 객체(프로그램의 데이터 구조)를 저장하거나 전송할 수 있는 형태로 변환하는 것이고, 역직렬화는 그 반대입니다. 애플리케이션이 신뢰할 수 없는 출처(사용자 입력 등)의 직렬화된 데이터를 역직렬화할 때, 만약 그 처리 과정이 안전하지 않으면, 공격자가 조작된 데이터를 통해 의도하지 않은 동작 — 심한 경우 임의 코드 실행 — 을 유발할 수 있습니다.
방어 원리는 명확합니다. 신뢰할 수 없는 데이터를 역직렬화하는 것을 피하고, 불가피하다면 무결성 검증과 안전한 역직렬화 방식을 사용하며, 역직렬화 과정에서 위험한 동작이 일어나지 않도록 제한하는 것입니다. 실무적으로는, 객체를 통째로 직렬화/역직렬화하는 위험한 포맷 대신 JSON처럼 단순한 데이터 형식만 주고받는 것이 가장 안전합니다. 언어별 안전한 처리 방법은 OWASP Deserialization Cheat Sheet에 정리돼 있습니다.
API 보안
현대의 웹은 API를 중심으로 돌아가는 경우가 많습니다. API도 웹 애플리케이션의 일부이며, 위에서 다룬 모든 취약점(인젝션, 인가 누락, SSRF 등)에 동일하게 노출됩니다. 특히 API는 종종 화면(UI) 없이 데이터를 직접 주고받으므로, 인가 확인이 누락되기 쉽고, 과도한 데이터를 노출하기 쉽습니다. 흔한 함정은, 프론트엔드가 화면에 안 보여 주니 안전하다고 착각하는 것입니다. 그러나 API 응답은 브라우저 개발자 도구로 누구나 그대로 볼 수 있습니다 — 화면에서 가렸다고 데이터가 숨겨진 게 아닙니다. API의 각 엔드포인트에 대해서도 12.3의 인가 원칙을 철저히 적용해야 하며, 필요 이상의 데이터를 응답에 담지 않도록 주의해야 합니다(특히 비밀번호 해시·내부 식별자·권한 플래그 같은 필드가 응답에 새지 않는지 확인). REST API의 보안 점검 항목은 OWASP REST Security Cheat Sheet에 정리돼 있습니다.
12.6 비즈니스 로직 결함: 스캐너가 결코 잡지 못하는 것
이제 이 장에서 가장 미묘하고, 가장 간과되는 부류를 다룹니다. 비즈니스 로직 결함(business logic flaw)입니다.
무엇이 다른가
지금까지 다룬 취약점들 — 인젝션, XSS, SSRF 등 — 은 기술적 결함입니다. 코드의 특정 패턴이 문제이고, 따라서 자동화된 도구가 어느 정도 탐지할 수 있습니다. 10장의 스캐너들이 잡아내는 것이 주로 이런 기술적 취약점입니다.
비즈니스 로직 결함은 다릅니다. 이것은 코드의 기술적 결함이 아니라, 애플리케이션의 논리 자체의 결함입니다. 코드는 정확히 설계된 대로 동작하지만, 그 설계 자체에 허점이 있는 경우입니다. 그래서 스캐너가 결코 잡지 못합니다. 스캐너는 "이 코드가 안전한가"는 점검할 수 있어도, "이 비즈니스 규칙이 악용될 수 있는가"는 이해하지 못하기 때문입니다.
예시로 이해하기
구체적 예로 이해하는 것이 가장 좋습니다. 비즈니스 로직 결함은 대략 이런 형태들입니다.
어떤 절차가 정해진 순서대로 진행되어야 하는데, 공격자가 중간 단계를 건너뛰어 의도하지 않은 상태에 도달하는 경우. 예를 들어 결제를 완료하지 않고도 결제 완료 상태에 이르는 경로가 있는 경우입니다.
수량이나 금액에 대한 검증이 한쪽으로만 되어 있어, 음수나 비정상적 값으로 의도하지 않은 결과를 얻는 경우. 예를 들어 음수 수량으로 잔액이 늘어나는 경우입니다.
한 번만 사용해야 할 것을 여러 번 사용하거나, 정해진 한도를 우회하는 경우. 예를 들어 일회용 할인을 반복 적용하는 경로가 있는 경우입니다.
권한이나 소유권에 대한 가정이 깨지는 경우. 예를 들어 자신의 것만 수정할 수 있어야 하는데, 논리적 허점으로 남의 것을 수정하는 경우입니다(12.3의 접근 제어와도 닿습니다).
이 모든 것의 공통점은, 각 단계의 코드는 "정상"이지만, 그것들이 조합되거나 예상치 못한 순서로 사용될 때 논리가 무너진다는 것입니다.
방어 원리: 사람의 사고가 필요하다
비즈니스 로직 결함에 대한 방어는 자동화할 수 없습니다. 그것은 사람의 사고를 요구합니다.
핵심은, 기능을 설계하고 검토할 때 "악의적인 사용자라면 이것을 어떻게 악용할까"를 상상하는 것입니다. 정상적인 흐름만 생각하지 말고, 비정상적인 입력, 예상치 못한 순서, 경계값, 동시 실행 등 공격자의 관점에서 그 논리를 시험해 보아야 합니다. 모든 중요한 동작에 대해, 그것이 일어나야 할 조건이 실제로 충족되었는지를 서버에서 엄격히 확인해야 합니다.
이것은 2장에서 다룬 위협 모델링의 정신을 기능 수준에서 적용하는 것입니다. 그리고 11.6에서 본 약점의 연쇄와도 닿습니다. 비즈니스 로직 결함은 종종 다른 약점과 엮여 큰 침해가 됩니다. 부록 E에서 다룬 AI 코드 검토가 기술적 취약점을 찾는 데 도움이 되지만, 비즈니스 로직 결함만큼은 그 애플리케이션의 의도를 깊이 이해하는 사람의 검토가 특히 중요합니다.
12.7 프레임워크와 의존성: 최신이라고 안전하지 않다
4장에서 다룬 의존성과 공급망 위험이 웹 애플리케이션에서 어떻게 구체화되는지를 짚습니다.
프레임워크 자체의 취약점
현대의 웹 애플리케이션은 대부분 프레임워크 위에서 만들어집니다. 프레임워크는 많은 보안 기능을 기본으로 제공하여 개발자를 돕습니다 — 앞서 본 매개변수화된 질의, 출력 인코딩, 인증 체계 등이 그 예입니다. 이것은 큰 장점입니다.
그러나 프레임워크 자체도 소프트웨어이며, 따라서 취약점을 가질 수 있습니다. 그리고 4장에서 강조했듯, 널리 쓰이는 프레임워크일수록 더 많은 연구자와 공격자의 주목을 받아, 취약점이 발견되고 공개되는 일이 주기적으로 일어납니다. 인기 있는 프레임워크에서 심각한 취약점이 공개되면, 그것을 쓰는 수많은 사이트가 동시에 위험에 노출됩니다. 공격자는 공개된 취약점을 빠르게 무기화하여(3장의 AI 가속), 해당 프레임워크를 쓰는 사이트들을 대량으로 노립니다(4장의 n-day 위협).
"최신 프레임워크니까 안전하다"는 착각
여기서 4장의 교훈을 다시 강조합니다. 최신의 현대적 프레임워크를 쓴다고 해서 안전이 보장되는 것이 아닙니다. 그 프레임워크에도 취약점이 발견되며, 중요한 것은 그것을 최신 패치 상태로 유지하는 것입니다. 프레임워크에 보안 패치가 나오면 신속히 적용해야 합니다(4장의 골든타임). 오히려 활발히 개발되는 프레임워크일수록 패치도 자주 나오므로, 그 패치를 따라가는 부지런함이 필요합니다.
실제로, 널리 쓰이는 프레임워크에서 원격 코드 실행 같은 치명적 취약점이 공개되는 사례가 반복되어 왔습니다. 멀리 갈 것도 없이 최근 몇 해의 사례만 봐도 그렇습니다.
- Spring4Shell(CVE-2022-22965) — 자바 진영에서 가장 널리 쓰이는 Spring 프레임워크의 원격 코드 실행 취약점. 공개되자마자 공격자들이 인터넷을 훑으며 대량으로 노렸습니다.
- Log4Shell(CVE-2021-44228) — 거의 모든 자바 애플리케이션이 의존하는 로깅 라이브러리 Log4j의 RCE. n-day의 교과서라 불릴 만큼, 패치가 나온 뒤에도 수년간 악용이 이어졌습니다(이것은 4장의 "의존성 빙산"이 왜 무서운지 보여 주는 사례이기도 합니다).
- Laravel Ignition(CVE-2021-3129) — PHP 프레임워크 Laravel의 디버그 화면(Ignition)에서 비롯된 RCE. 운영 서버를
APP_DEBUG=true로 켜 둔 환경이 특히 위험했습니다(11장에서 다룬 디버그 모드 노출이 어떻게 침해로 이어지는지의 실제 예).
이런 취약점이 공개되면, 패치하지 않은 사이트는 곧 자동화된 대량 공격의 대상이 됩니다. 패치가 나와 있는데도 적용하지 않아 뚫리는 것 — 4장에서 강조한 n-day의 위험이 프레임워크에서 가장 빈번하게 현실화됩니다. 어떤 취약점이 "이론"이 아니라 "지금 실제로 악용되고 있는지"는 CISA의 KEV(악용 취약점 목록)에서 확인할 수 있습니다. 여기 오른 항목은 최우선으로 패치해야 합니다 — 위에 든 셋은 모두 이 목록에 올랐던 것들입니다.
방어 원리
방어는 4장과 5장에서 다룬 그대로입니다. 자신이 쓰는 프레임워크와 그 버전을 파악하고(5장 인벤토리), 의존성의 빙산 전체를 점검하며(4장), 보안 패치가 나오면 신속히 적용하고(4장 골든타임), 이 점검과 패치를 정기 루틴과 자동화로 만드는 것입니다(원칙 5, 6, 부록 E). 그리고 프레임워크가 제공하는 보안 기능을 우회하지 말고 올바르게 사용하는 것 — 이것이 이 장 전체를 관통하는 원칙입니다.
내 버전이 취약한지 어떻게 아나. 의존성 목록(예:
composer.lock,package-lock.json)을 들고 다음을 조회하면 됩니다. 특정 패키지·버전이 영향받는지는 OSV나 GitHub Advisory Database에서, 취약점의 상세와 심각도(CVSS)는 NVD에서 확인합니다. 더 편한 길은 자동화입니다 —npm audit,composer audit같은 내장 명령이나 GitHub Dependabot을 켜 두면 취약한 의존성이 들어올 때 알려 줍니다. 한 번 점검으로 끝내지 말고, 이 알림을 상시로 켜 두는 것이 핵심입니다.
12.8 이 장이 남기는 교훈
이 장에서 확인한 것을 압축합니다.
첫째, 웹 애플리케이션은 인프라 방어를 통과한 공격이 들어오는 마지막이자 가장 넓은 전선입니다. 방화벽도 TLS도 닫힌 포트도 웹 취약점을 막지 못합니다. 정문으로 들어오는 공격은 애플리케이션 자신이 막아야 합니다.
둘째, 인젝션과 XSS의 본질은 같습니다. 데이터가 명령이나 코드로 착각되는 것입니다. 방어의 핵심은 데이터와 명령의 분리(매개변수화된 질의), 그리고 출력 인코딩입니다. 프레임워크의 기본 보호를 우회하지 마십시오.
셋째, 인증과 인가는 다르며, 인가 누락이 큰 위험입니다. 모든 자원 접근에 대해, 매번, 서버 측에서, 기본 거부 원칙으로 인가를 확인하십시오. 클라이언트 측 제약은 인가가 아닙니다.
넷째, SSRF는 서버를 발판으로 내부를 공격하며, 클라우드에서 치명적입니다. 메타데이터 탈취로 클라우드 전체가 넘어갈 수 있습니다. 요청 대상을 제한하고 최소 권한을 적용하십시오.
다섯째, 비즈니스 로직 결함은 스캐너가 잡지 못합니다. 코드는 정상이지만 논리에 허점이 있는 것으로, "악의적 사용자라면 어떻게 악용할까"를 상상하는 사람의 사고가 필요합니다.
여섯째, 최신 프레임워크도 안전하지 않습니다. 모든 프레임워크에 취약점이 발견되며, 중요한 것은 신속한 패치입니다. 패치가 나왔는데 적용하지 않아 뚫리는 n-day의 위험이 프레임워크에서 가장 빈번합니다.
이것으로 4부 "검증과 공격적 보안"을 마칩니다. 우리는 자신을 스캔하고(10장), 펜테스트의 시선으로 흔한 약점을 보고(11장), 웹 애플리케이션의 취약점을 체계적으로 다뤘습니다(12장). 그러나 이 모든 방어에도 불구하고, 이 책의 두 번째 원칙은 말합니다. 침해를 가정하라. 완벽한 예방은 없습니다. 그렇다면 뚫렸을 때 어떻게 할 것인가. 마지막 5부에서, 우리는 탐지와 대응, 그리고 회복력으로 시선을 옮깁니다. 다음 장에서 침해를 가정한 방어 — 로깅, 모니터링, 백업, 사고 대응 — 를 다루겠습니다.
이 장의 실행 항목
- 인젝션 방어를 점검하십시오. 매개변수화된 질의를 쓰는지, 사용자 입력을 질의문에 직접 잇지 않는지 확인하십시오. (12.1)
- XSS 방어를 점검하십시오. 출력 인코딩이 적용되는지, 프레임워크의 자동 인코딩을 우회하지 않는지 확인하고, CSP를 더하십시오. (12.2, 9장)
- 접근 제어를 점검하십시오. 모든 자원 접근에 서버 측 인가 확인이 있는지, 기본 거부 원칙을 따르는지 확인하십시오. (12.3)
- SSRF 위험을 점검하십시오. 서버가 임의 주소로 요청을 보낼 수 있는지, 클라우드 메타데이터 접근이 보호되는지 확인하십시오. (12.4)
- 비즈니스 로직을 공격자 관점에서 검토하십시오. "악의적 사용자라면 어떻게 악용할까"를 상상하십시오. 스캐너는 이것을 못 잡습니다. (12.6)
- 프레임워크와 의존성을 최신 패치 상태로 유지하십시오. 최신이라고 안전한 것이 아닙니다. 신속한 패치가 핵심입니다. (12.7, 4장)
- 프레임워크의 보안 기능을 우회하지 말고 올바르게 쓰십시오. 이것이 이 장 전체의 원칙입니다.
- 여기까지를 도구로 검증하십시오. 위 항목들을 사람의 눈으로만 점검하기는 벅찹니다. 자신의 웹앱을 대상으로 OWASP ZAP(무료 오픈소스 웹 취약점 스캐너)를 돌려 보고, 더 깊이 보려면 Burp Suite를 씁니다. 둘 다 브라우저와 서버 사이의 요청을 가로채 들여다보는 프록시 도구라, 12.3의 "서버에 직접 요청을 보내면 인가가 뚫리나"를 직접 확인하기에 좋습니다(반드시 본인 소유·허가된 대상에만). 그리고 빠진 항목이 없는지 OWASP ASVS 체크리스트로 대조하십시오. (10장, 11장)
다음 장: 13장 — 침해를 가정하라. 탐지와 대응, 백업과 회복력. 5부 "운영과 생존"의 시작.