13년차의 서버실

서버, 인프라, 홈랩 기술 블로그

[태그:] OWASP Top 10

  • [보안] 웹 애플리케이션 보안 체크리스트: 개발자 배포 전 점검

    [보안] 웹 애플리케이션 보안 체크리스트: 개발자 배포 전 점검

    웹 애플리케이션 보안 체크리스트를 벽에 붙여두기만 하고 배포 게이트로 쓰지 않으면, 사고는 대개 어려운 취약점보다 빼먹은 기본 설정에서 납니다. 저도 여러 팀에서 비슷한 장면을 반복해서 봤습니다. 로그인은 붙었는데 세션 쿠키 속성이 비어 있고, 파일 업로드는 되는데 저장 위치가 웹 루트 안쪽이고, API는 인증이 있는데 객체 단위 권한 검사가 빠져 있는 식이었죠. WAF(Web Application Firewall, 웹 애플리케이션 방화벽)나 클라우드 보안 서비스가 있어도 이 층의 누락은 결국 애플리케이션 팀이 책임지게 되더라고요.

    본 글은 보안 학습과 본인이 관리하는 시스템의 방어를 위한 교육 목적입니다. 타인의 시스템에 대한 무단 접근·공격은 불법이며 법적 처벌 대상입니다. 여기서는 웹 애플리케이션 보안 체크리스트를 단순 항목 나열이 아니라, 배포 직전 무엇을 어떤 기준으로 통과시킬지에 맞춰 다시 묶어보겠습니다. 특히 개발자 입장에서 자주 헷갈리는 지점, 예외를 허용해도 되는 조건, 장애를 만들지 않고 강도를 높이는 순서를 같이 적었습니다.

    웹 애플리케이션 보안 체크리스트 전체 흐름 아키텍처 이미지

    개발, 배포, 운영 단계에서 인증, 입력값 검증, 비밀정보 관리, 로깅, 취약점 스캔이 연결되는 전체 흐름을 보여주는 이미지입니다.

    웹 애플리케이션 보안 체크리스트가 왜 필요한가

    체크리스트는 문서가 아니라 릴리스 품질 게이트여야 합니다. 제 기준으로는 세 번 걸어야 의미가 있습니다. 첫째, PR(Pull Request) 리뷰에서 설계 결함을 걸러냅니다. 둘째, CI/CD(Continuous Integration/Continuous Deployment, 지속적 통합/배포)에서 자동으로 확인 가능한 항목을 실패 처리합니다. 셋째, 운영 반영 직전에 환경 차이로 누락된 설정을 다시 봅니다. 이 셋 중 하나라도 빠지면 개발 환경에서는 괜찮았는데 운영에서만 새는 문제가 남습니다.

    현장에서 자주 터지는 실패 모드는 생각보다 단순합니다. 임시 우회 설정이 운영까지 남아 있거나, 프레임워크 기본값을 과신하거나, 스캔 도구 결과를 숫자로만 읽고 실제 실행 경로를 보지 않는 경우가 많습니다. 근본 원인은 거의 늘 같습니다. 보안 항목이 기능 정의서에는 없는데 운영 책임에는 포함돼 있기 때문입니다. 그래서 체크리스트는 책임 소재를 분명히 하는 문서이기도 합니다. 누가 보고, 언제 막고, 어떤 경우 예외를 허용하는지까지 있어야 흔들리지 않습니다.

    웹 애플리케이션 보안 체크리스트에서 보는 OWASP Top 10

    OWASP Top 10을 그대로 외우는 건 실무에서 큰 도움이 안 될 때가 많습니다. OWASP도 이 목록을 최소 기준이자 출발점에 가깝다고 설명합니다. 개발자는 위험 범주를 코드와 설정의 점검 포인트로 번역해서 보는 편이 훨씬 실용적입니다. 제가 팀 문서로 옮길 때는 아래처럼 바꿔 적습니다.

    • Broken Access Control(접근 제어 실패): 요청 단위가 아니라 객체 단위로 권한을 확인하는지 봅니다.
    • Cryptographic Failures(암호화 실패): TLS 종료 지점, 쿠키 보안 속성, 저장 시 암호화 대상이 맞는지 봅니다.
    • Injection(인젝션): 쿼리 생성, 명령 실행, 템플릿 렌더링에서 입력이 코드로 해석될 여지가 있는지 봅니다.
    • Security Misconfiguration(보안 설정 오류): 디버그 모드, 기본 계정, 관리 포트 노출, 과한 CORS(Cross-Origin Resource Sharing) 허용을 봅니다.
    • Vulnerable and Outdated Components(취약하거나 오래된 구성요소): 취약 버전 자체보다 실행 경로에 실제로 닿는지, 직접 의존성인지 먼저 봅니다.
    • Identification and Authentication Failures(식별 및 인증 실패): 세션 고정, 토큰 만료, 재인증 정책, 관리자 기능 보호를 봅니다.

    이렇게 바꿔 놓으면 보안팀 용어를 개발 작업으로 연결하기 쉬워집니다. 제 경험상 팀이 가장 빨리 개선하는 영역도 여기입니다. 보안 이슈라고 적는 순간 막연해지는데, 이 API는 객체 소유자 검사가 빠져 있다고 쓰면 바로 수정 단위가 되거든요.

    실무에서 바로 쓰는 웹 애플리케이션 보안 체크리스트

    항목은 많아 보여도, 배포 전에 정말 체크할 것만 남기면 의외로 운영 가능합니다. 저는 아래 10개를 기본 축으로 둡니다. 관리자 페이지, 일반 사용자 서비스, 내부 API까지 거의 공통으로 먹힙니다.

    1. 인증과 세션: 세션 쿠키에 HttpOnly, Secure, SameSite가 있는지, 세션 만료와 재발급 정책이 분리돼 있는지 봅니다.
    2. 인가와 권한: 화면 숨김과 별개로 서버가 리소스별 권한을 검증하는지 봅니다.
    3. 입력값 검증: 허용 목록 기준인지, 길이 제한과 타입 검사가 서버에서 강제되는지 봅니다.
    4. 출력 인코딩: HTML, URL, JavaScript 문자열, JSON 응답 문맥을 섞지 않는지 봅니다.
    5. 비밀정보 관리: 코드 저장소, 빌드 로그, 런타임 환경 변수 출력, 에러 리포트에 시크릿이 남지 않는지 봅니다.
    6. 의존성 관리: 취약 패키지 유무보다 수정 가능 여부, 직접 의존성 여부, 런타임 노출 여부를 같이 봅니다.
    7. 보안 헤더: Content-Security-Policy, X-Content-Type-Options, Referrer-Policy가 실제 응답에 붙는지 확인합니다.
    8. 로깅과 모니터링: 로그인 실패, 권한 거부, 관리자 기능 실행, 비정상 오류는 남기되 민감정보는 마스킹합니다.
    9. 에러 처리: 사용자 응답은 단순하게, 내부 로그는 추적 가능하게 분리돼 있는지 봅니다.
    10. 파일 업로드: 확장자뿐 아니라 MIME 타입, 저장 경로, 후처리 파이프라인, 실행 권한을 같이 봅니다.

    웹 애플리케이션 보안 체크리스트 우선순위와 판정 기준

    영역 지금 바로 볼 것 배포 차단 기준 예외를 허용해도 되는 경우 권장 조치
    인증 세션 쿠키 속성, 세션 만료 정책 운영 HTTPS에서 Secure 누락, 관리자 세션 장기 유지, 재로그인 없이 민감 작업 가능 개발 로컬 환경의 비TLS 테스트 정도 프레임워크 설정에서 강제하고, 관리자 기능은 재인증 또는 짧은 세션 적용
    인가 객체 단위 접근 제어 다른 사용자 데이터가 서버 응답에 포함됨 없음 리소스 소유자 검증을 서비스 계층 또는 정책 미들웨어로 공통화
    입력값 서버 검증, 길이 제한 클라이언트 검증 제거 시 서버가 그대로 수용 내부 전용 도구라도 서버 검증은 유지 DTO/스키마 검증과 DB 제약조건을 함께 사용
    시크릿 코드 저장소, 로그, 환경 변수 노출 토큰·비밀번호가 저장소 또는 로그에 평문 존재 없음 시크릿 매니저 사용, 로그 마스킹 필터 적용
    의존성 직접 의존성의 high/critical 취약점 인터넷 노출 런타임 경로에서 수정 가능한 고위험 취약점 방치 패치가 없을 때 보완 통제와 만료일을 명시한 단기 예외 직접 의존성 우선 업데이트, 간접 의존성은 상위 패키지 교체 검토
    로깅 민감정보 포함 여부, request id 토큰 전문, 비밀번호, 주민등록번호류가 그대로 기록 없음 마스킹 규칙과 구조화 로그 키 표준화

    체크리스트를 돌릴 때 제가 고정으로 보는 결정 포인트

    보안 점검은 좋은 설정을 모으는 일이 아니라 충돌하는 목표 사이에서 우선순위를 정하는 일에 가깝습니다. 예를 들어 CSP(Content Security Policy)는 강하게 잠글수록 안전하지만, 이미 인라인 스크립트와 다중 CDN에 기대고 있는 서비스라면 한 번에 강제 적용하면 장애가 납니다. 반대로 SameSite 쿠키는 보수적으로 잡을수록 좋지만 외부 인증 연동이 있으면 리다이렉트 흐름을 깨뜨릴 수 있습니다. 이 지점에서 성급하게 밀어붙이면 보안팀도 개발팀도 둘 다 지치더라고요.

    상황 A를 택할 때 B를 택할 때 제 추천
    CSP 바로 강제 vs Report-Only부터 시작 리소스 출처가 단순하고 인라인 스크립트 제거가 끝난 경우 외부 스크립트가 많고 프런트 구조를 아직 정리 못한 경우 신규 서비스는 강제, 기존 서비스는 Content-Security-Policy-Report-Only로 먼저 수집 후 강제 전환
    세션 기반 인증 vs 토큰 기반 인증 서버 렌더링 중심, 브라우저 세션 관리가 단순한 경우 다수의 API 클라이언트, 모바일 앱, 서비스 간 호출이 있는 경우 브라우저 웹앱은 세션 쿠키 우선, 다중 클라이언트 API는 짧은 만료 토큰과 서버 측 권한 검증 병행
    취약점 스캔 즉시 차단 vs 예외 후 추적 직접 의존성, 수정 가능, 인터넷 노출 런타임인 경우 패치가 없고 실제 경로 노출이 없으며 보완 통제가 있는 경우 기계적으로 전부 차단하지 말고, 예외는 만료일과 대체 통제를 문서화
    파일 업로드 즉시 공개 저장 vs 비공개 저장 후 서빙 공개 정적 자산이며 서버 측 재처리가 필요 없는 경우 사용자 업로드, 개인정보, 후처리 검사가 필요한 경우 사용자 업로드는 웹 루트 밖 또는 비공개 버킷 저장 후 검증된 경로로만 제공

    실전 구현 1: HTTP 보안 헤더와 쿠키 설정

    헤더는 비용 대비 효과가 좋습니다. 다만 추가했다와 제대로 적용됐다는 완전히 다른 얘기입니다. 리버스 프록시에서 넣었는데 애플리케이션 서버가 덮어쓰는 경우도 있고, CDN이 일부 헤더를 제거하는 경우도 있습니다. 제 기준으로는 응답에 실제로 존재하는지, 브라우저 콘솔에서 정책 충돌이 없는지까지 봐야 끝입니다. 이거 확인 안 하면 겉으로만 안전해 보이는 상태가 은근 자주 남습니다.

    server {
        listen 443 ssl http2;
        server_name example.com;
    
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-Frame-Options "DENY" always;
        add_header Referrer-Policy "strict-origin-when-cross-origin" always;
        add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
        add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests" always;
    
        # nginx 1.19.3+ 기준
        proxy_cookie_flags ~ secure httponly samesite=lax;
    
        location / {
            proxy_pass http://app_upstream;
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header X-Request-Id $request_id;
        }
    }

    여기서 주의할 점도 분명합니다. X-Frame-Options: DENY는 관리 콘솔이나 외부 임베드 요구가 있으면 충돌할 수 있습니다. SameSite=Strict는 가장 보수적이지만 외부 로그인 연동이나 결제 리다이렉트 흐름을 깨뜨릴 수 있어, 일반적인 웹앱은 Lax부터 검토하는 편이 현실적입니다. style-src 'unsafe-inline'은 이상적이진 않지만, 기존 서비스에서 인라인 스타일 제거가 안 끝났다면 임시로 두고 범위를 줄여가는 식이 보통 더 안전합니다.

    검증은 아래 정도면 충분합니다. 중요한 건 한 번 찍고 끝내지 않고, 로그인 전후 응답과 주요 페이지 응답을 각각 보는 겁니다. 쿠키 속성은 인증 이후에야 보이는 경우가 많거든요.

    curl -I https://example.com
    curl -s -D - -o /dev/null https://example.com | grep -Ei 'content-security-policy|x-frame-options|x-content-type-options|referrer-policy|permissions-policy'
    
    # 로그인 후에는 브라우저 개발자 도구 또는 프록시에서 Set-Cookie 속성을 확인
    # 확인 포인트: HttpOnly; Secure; SameSite=Lax 또는 SameSite=Strict

    판단 기준도 애매하게 두지 않는 편이 좋습니다. 보안 헤더가 아예 없으면 설정 누락으로 보고 바로 수정합니다. CSP 위반 로그가 다수 보이면 정책이 앱 구조와 맞지 않는다는 뜻이지, 무조건 완화하라는 의미는 아닙니다. 어떤 스크립트나 스타일이 어디서 로드되는지부터 분류해야 합니다. 관련 글이 있다면 여기서 보안 헤더 가이드나 OWASP Top 10 해설로 내부 링크를 연결해 두면 검색 유입에도 도움이 됩니다.

    웹 애플리케이션 보안 체크리스트에서 보안 헤더 구성을 설명하는 이미지

    리버스 프록시에서 보안 헤더를 추가하고, 뒤쪽 애플리케이션으로 요청을 전달하는 구조를 설명하는 이미지입니다.

    실전 구현 2: 의존성 취약점 점검과 결과 읽는 법

    의존성 스캔은 돌렸느냐보다 읽을 줄 아느냐가 더 중요합니다. 현업에서 진짜 시간을 잡아먹는 건 경고 숫자보다 우선순위 판단입니다. 저는 세 가지만 먼저 봅니다. 직접 의존성인가, 수정 가능한가, 운영 런타임에 실제로 실리나. 이 순서로 보면 노이즈가 꽤 줄어듭니다.

    # Node.js
    npm audit --audit-level=high
    npm outdated
    npm ls --depth=2
    
    # Python 3.10+ 환경 예시
    python -m pip install --upgrade pip
    python -m pip install pip-audit
    pip-audit
    python -m pip list --outdated

    npm audit 결과에서 fix available가 있으면 예외 등록보다 업데이트 가능성부터 보는 게 맞습니다. 다만 자동 수정이 항상 정답은 아닙니다. 메이저 버전 상승이 포함되면 빌드나 런타임 동작이 바뀔 수 있으니까요. 저는 직접 의존성이면 changelog를 확인하고 테스트가 있는 범위에서 먼저 올리고, 간접 의존성이면 상위 패키지 업데이트로 해결되는지부터 봅니다.

    초반에 제가 가장 많이 실수했던 것도 여기였습니다. CI에서 high 이상이면 전부 배포 차단으로 걸어두니, 결국 개발팀이 예외 파일만 늘리더라고요. 그 뒤부터는 룰을 바꿨습니다. 운영 노출 경로 + 수정 가능 + 직접 의존성이면 차단, 그 외는 보완 통제와 일정 관리로 보되 만료일 없는 예외는 금지. 이 기준이 있어야 스캔 도구가 업무를 막는 장치가 아니라 우선순위 도구가 됩니다.

    결과 해석 기준

    • high 또는 critical이라도 개발 전용 패키지인지 운영 경로인지 먼저 구분합니다.
    • fix available가 있으면 예외보다 업데이트를 우선합니다.
    • 같은 루트 패키지에서 파생된 경고는 묶어서 봐야 실제 작업량이 보입니다.
    • 패치가 없으면 차단보다 보완 통제를 문서화하고 재검토 일정을 박아두는 편이 낫습니다.

    실전 구현 3: 시크릿 관리와 로그 마스킹

    시크릿은 코드에만 안 넣으면 끝이라고 생각하기 쉬운데, 실제 누출 경로는 훨씬 많습니다. CI 로그, 컨테이너 시작 스크립트, 예외 스택, 디버그 엔드포인트, 운영자가 급히 남긴 임시 로그가 다 후보입니다. 특히 장애 대응 중에 잠깐 보자고 찍은 로그가 오래 남는 경우가 정말 많습니다. 이 부분은 한 번 터지면 수습이 꽤 번거롭더라고요.

    # 런타임 환경 변수 이름만 점검
    printenv | cut -d= -f1 | grep -Ei 'token|secret|password|key'
    
    # 저장소 내 하드코딩 흔적 점검
    git grep -nE '(API_KEY|SECRET_KEY|PASSWORD|TOKEN|ACCESS_KEY|PRIVATE_KEY)'
    
    # 로그 샘플에서 민감 키 이름 확인
    grep -RniE 'authorization:|bearer |set-cookie|password|secret|token' ./logs 2>/dev/null

    이 명령은 어디까지나 자신이 관리하는 시스템의 방어 점검용입니다. 결과를 외부에 공유하거나 협업 채널에 그대로 붙이면 안 됩니다. 제 경험상 가장 안전한 운영 방식은 두 가지입니다. 첫째, 시크릿 값 자체를 로그에 남기지 않도록 애플리케이션 레벨에서 필터링합니다. 둘째, 장애 분석에 필요한 식별자는 토큰 전체가 아니라 해시나 앞뒤 일부만 남깁니다.

    예를 들어 로그에 Authorization 헤더 전체를 남기면 분석은 편하지만 유출 리스크가 너무 큽니다. 반대로 아무것도 안 남기면 장애 추적이 막힙니다. 저는 보통 request id, 사용자 내부 식별자, 권한 결과, 실패 원인 코드까지만 남기고, 비밀번호·세션 ID·Bearer 토큰 전문은 금지합니다. 여기서 중요한 건 민감정보를 빼자가 아니라 분석 가능한 최소 정보만 남기자는 원칙입니다.

    재현 가능한 점검 시나리오: 권한 검증 누락 찾기

    추상적인 원칙보다 실제 점검 절차 하나가 더 오래 남습니다. 그래서 저는 배포 전 권한 검증 확인을 꼭 시나리오로 남깁니다. 대상은 관리자 페이지, 주문 조회, 프로필 수정, 첨부파일 다운로드처럼 사용자별 소유권이 분명한 기능입니다. 절차는 복잡하지 않습니다. 내가 관리하는 테스트 환경에서 정상 계정으로 로그인하고, 네트워크 탭에서 해당 요청의 URL, 메서드, 응답 코드를 확인합니다. 그런 다음 같은 계정 상태에서, 시스템이 허용하지 않아야 하는 다른 리소스에 대한 접근 요청이 서버에서 차단되는지만 봅니다.

    여기서 핵심은 우회 기법을 늘어놓는 게 아니라 서버가 객체 소유권을 기준으로 차단하는지 확인하는 데 있습니다. 정상이라면 403 Forbidden 또는 서비스 정책에 맞는 거부 응답이 나와야 합니다. 만약 화면에서는 버튼이 숨겨져 있는데 응답 자체는 나온다면, 그건 거의 전형적인 Broken Access Control입니다. 프런트엔드 조건 분기와 서버 권한 검증을 같은 것으로 취급한 결과죠.

    이 시나리오가 좋은 이유는 재현성이 있기 때문입니다. QA가 다시 돌리기 쉽고, 개발자가 수정 후 검증 기준으로 삼기 쉽습니다. 또 성능 부담도 거의 없습니다. 별도 스캐너 없이 브라우저 개발자 도구와 애플리케이션 로그만으로 충분히 확인 가능한 경우가 많습니다. 저는 이런 체크가 문서에 한 줄로만 적혀 있을 때보다, 어떤 화면에서 무엇을 봐야 하는지 적혀 있을 때 팀이 훨씬 덜 놓친다고 봅니다.

    웹 애플리케이션 보안 체크리스트의 접근 제어 검증 흐름 이미지

    정상 사용자 요청과 권한 없는 요청을 구분하고, 서버가 403으로 차단하는 흐름을 보여주는 이미지입니다.

    주의사항과 트러블슈팅

    보안 설정은 강하게 넣을수록 좋아 보이지만, 운영에서는 늘 부작용과 같이 옵니다. 그래서 저는 장애를 만드는 설정과 리스크를 줄이는 설정을 구분해서 적용합니다. 이 선을 잘 잡아야 팀이 보안을 싫어하지 않습니다.

    • CSP 적용 후 화면이 깨짐: 대개 근본 원인은 인라인 스크립트, 서드파티 CDN, 레거시 위젯입니다. 이럴 땐 정책을 즉시 느슨하게 풀기보다 Report-Only로 위반 출처를 수집하고, 인라인 제거 작업부터 시작하는 편이 낫습니다.
    • SameSite 설정 후 로그인 연동 문제: OAuth 2.0, SSO(Single Sign-On), 결제 리다이렉트가 있으면 Strict가 과할 수 있습니다. 외부 리다이렉트가 있는 웹앱은 먼저 Lax를 검토하세요.
    • 취약점 스캔 결과가 너무 많음: 근본 원인은 중복 경고와 간접 의존성입니다. 루트 패키지별로 묶고, 직접 의존성부터 줄이는 편이 실제 효과가 큽니다.
    • 에러를 숨겼더니 운영 분석이 어려움: 사용자 응답과 내부 로그를 분리하지 않았기 때문입니다. 외부 응답은 일반화하고, 내부에는 request id, 예외 클래스, 상위 원인만 남기면 됩니다.

    파일 업로드는 특히 낙관적으로 보면 안 됩니다. 확장자 검사만으로는 부족하고, 저장 위치가 웹 루트 안쪽이면 더 위험합니다. 제 추천은 분명합니다. 사용자 업로드는 웹에서 직접 실행되지 않는 위치에 저장하고, 필요하면 별도 서빙 계층이나 비공개 스토리지의 서명 URL 같은 방식으로 노출하세요. 이미지라면 업로드 직후 원본을 그대로 노출하기보다 후처리 파이프라인을 거친 파생본만 서빙하는 편이 더 안전합니다. 원본 메타데이터(EXIF 등) 처리 여부도 같이 확인하셔야 합니다.

    웹 애플리케이션 보안 체크리스트 검증: 배포 전에 무엇을 확인하면 되나

    배포 직전 검증은 길게 할 필요 없습니다. 대신 짧아도 빠뜨리면 안 되는 순서가 있습니다. 제가 운영 중인 서비스에서 주로 쓰는 순서는 아래와 같습니다. 이 순서가 좋은 이유는 헤더, 권한, 의존성, 로그 노출처럼 서로 다른 계층을 짧은 시간 안에 교차 검증할 수 있기 때문입니다.

    1. 주요 응답 헤더 확인: curl -I와 실제 로그인 후 응답에서 헤더와 쿠키 속성을 봅니다.
    2. 로그인/권한 흐름 확인: 일반 사용자와 관리자 요청이 서버에서 의도대로 구분되는지 봅니다.
    3. 취약점 스캔 결과 확인: 직접 의존성, 수정 가능, 런타임 노출 여부로 우선순위를 정합니다.
    4. 로그 샘플 확인: 토큰, 비밀번호, 개인정보가 평문으로 남지 않는지 확인합니다.
    5. 에러 페이지 확인: 내부 경로, 프레임워크 버전, 스택 트레이스 노출이 없는지 봅니다.

    이때 기준을 분명히 적어두면 팀이 덜 흔들립니다. 예를 들어 인증·권한 실패는 예외 없이 수정 대상, 직접 의존성의 수정 가능한 고위험 취약점은 배포 전 우선 조치, 로그 민감정보 노출은 기능 영향과 무관하게 즉시 수정, CSP 경고는 실제 차단 영향과 위반 출처를 확인한 뒤 단계 적용. 이렇게 써두면 이번엔 바쁘니까 넘어가자는 말이 줄어듭니다.

    # 배포 전 방어 점검 예시 순서
    curl -I https://example.com
    curl -s -D - -o /dev/null https://example.com/login | grep -Ei 'set-cookie|content-security-policy|x-frame-options|x-content-type-options'
    npm audit --audit-level=high
    pip-audit
    git grep -nE '(API_KEY|SECRET_KEY|PASSWORD|TOKEN)'
    # 로그 샘플은 운영 정책에 따라 마스킹된 사본으로만 확인

    여기서 하나 더 중요합니다. 자동화 가능한 항목과 사람 판단이 필요한 항목을 섞지 않는 게 좋습니다. 헤더 존재 여부, 의존성 스캔, 하드코딩 흔적 검색은 CI에서 자동화하기 좋습니다. 반면 권한 설계의 타당성, CSP 위반 허용 범위, 예외 승인 타당성은 여전히 사람이 봐야 합니다. 둘을 구분하지 않으면 자동화가 과신으로 바뀌거나, 반대로 전부 수작업이 되어 금방 지칩니다.

    웹 애플리케이션 보안 체크리스트 검증 결과 대시보드 이미지

    헤더 점검, 의존성 스캔, 권한 검증, 로그 마스킹 상태를 한눈에 보여주는 결과 요약 이미지입니다.

    운영 문서로 남길 때 좋은 형태

    체크리스트는 문서함에 들어가면 죽습니다. 살아 있으려면 PR 템플릿, 배포 파이프라인, 장애 대응 문서에 각각 연결돼야 합니다. 제가 추천하는 방식은 문서를 길게 쓰는 게 아니라, 각 접점에 질문을 짧게 심는 겁니다. 예를 들어 PR 템플릿에는 새 API 추가 시 인증/인가 확인, 민감정보 로그 출력 없음, 시크릿 하드코딩 없음 정도만 있어도 효과가 큽니다. 배포 파이프라인에는 헤더 검사, 의존성 스캔, 기본 grep 검사를 자동화하고요.

    반대로 하지 말아야 할 것도 있습니다. 체크리스트를 보안팀 전용 문서로 분리해 두는 방식입니다. 그러면 개발자는 릴리스 압박을 먼저 받고, 보안 항목은 마지막 승인 절차로만 남습니다. 그 구조에서는 늘 늦습니다. 개발자가 수정할 수 있는 형태로, 배포 전에 다시 확인할 수 있는 문장으로 바꿔야 실제로 작동합니다.

    FAQ: 현업에서 자주 나오는 질문

    보안 도구를 도입했는데도 왜 불안할까요?

    도구는 탐지해주지만, 권한 설계와 예외 판단을 대신해주지는 않습니다. 특히 객체 단위 권한, 로그 민감정보, 운영 설정 차이는 도구만으로 다 걸러지지 않습니다. 그래서 웹 애플리케이션 보안 체크리스트가 필요합니다.

    개발 속도가 느려지지 않나요?

    초반에는 조금 느려집니다. 다만 운영 사고가 나면 기능 개발, 고객 대응, 로그 조사, 롤백까지 한 번에 붙습니다. 제가 체감한 차이는 단순했습니다. 초기에 배포 기준을 분명히 둔 팀이 결국 더 빨랐습니다. 재작업이 줄기 때문입니다.

    OWASP Top 10을 다 외워야 하나요?

    그럴 필요 없습니다. 인증, 권한, 입력값, 설정, 의존성 같은 개발 작업 단위로 번역해서 보시면 됩니다. 외우는 것보다 배포 체크 질문으로 바꾸는 편이 훨씬 실용적입니다.

    인증, 인가, 입력값 검증, 보안 헤더, 의존성 관리, 로그 마스킹을 핵심만 정리한 요약 이미지입니다.

    마무리: 상황별로 이렇게 가시면 됩니다

    이미 운영 중인 서비스에서 빠르게 리스크를 줄여야 한다면, 저는 권한 검증 확인 → 보안 헤더와 쿠키 속성 점검 → 의존성 스캔 우선순위 정리 → 시크릿·로그 점검 순서로 가시라고 말씀드립니다. 개인정보 처리 API나 관리자 기능이 있으면 접근 제어가 첫 번째입니다. 정적 페이지 비중이 큰 서비스라면 CSP와 배포 헤더 설정이 먼저고요. 외부 로그인 연동이 많다면 SameSite와 리다이렉트 흐름부터 조심해서 보셔야 합니다.

    신규 프로젝트라면 더 단순합니다. PR 템플릿에 체크 항목을 심고, CI에 자동 검사 가능한 부분을 붙이고, 운영 반영 직전에 권한과 로그 샘플만 사람이 다시 보면 됩니다. 이 순서가 가장 덜 지치고, 가장 오래 갑니다.

    보안은 거대한 장비보다 작은 누락에서 먼저 무너집니다. 본 글은 보안 학습과 본인이 관리하는 시스템의 방어를 위한 교육 목적이며, 타인의 시스템에 대한 무단 접근·공격은 불법이며 법적 처벌 대상입니다. 체크리스트를 문서로만 두지 말고 배포 흐름에 연결해 두세요. 그 순간부터 품질 관리가 아니라 사고 예방 장치가 됩니다.

  • [보안] OWASP ZAP으로 XSS 취약점 진단 및 해결: 실제 웹 애플리케이션 디버깅 사례

    [보안] OWASP ZAP으로 XSS 취약점 진단 및 해결: 실제 웹 애플리케이션 디버깅 사례

    [웹보안] OWASP ZAP으로 XSS 취약점 진단 및 해결

    OWASP ZAP XSS 진단은 웹 서비스를 운영하는 분이라면 한 번쯤 꼭 부딪히는 주제입니다. 기능 테스트는 다 끝났는데, 막상 보안 점검 단계에서 경고가 쏟아지면 머리가 좀 아파지거든요. 저도 홈랩에서 돌리던 작은 웹 애플리케이션이랑 사내 업무성 화면을 비슷한 방식으로 점검해보면서, “이거 사용자 입력만 잘 받으면 되는 거 아닌가?” 했다가 삽질 좀 했습니다 ㅎㅎ 특히 XSS(Cross-Site Scripting, 교차 사이트 스크립팅) 쪽은 화면이 멀쩡해 보여도 실제로는 브라우저에서 위험한 스크립트가 실행될 여지가 숨어 있는 경우가 많더라고요.

    이번 글에서는 OWASP ZAP XSS 점검을 어떻게 시작하면 좋은지, 어떤 식으로 취약점을 재현하고, 실제 웹 애플리케이션 디버깅 과정에서 무엇을 확인해야 하는지 제 경험 기준으로 정리해보겠습니다. 단순히 도구만 돌리는 글이 아니라, 웹 보안 취약점을 어떻게 읽고 해석할지까지 같이 보실 수 있게 구성했습니다.

    OWASP ZAP을 이용한 XSS 진단 전체 흐름 다이어그램

    브라우저, 프록시, 대상 웹 애플리케이션, 취약점 탐지 흐름을 한눈에 보여주는 개요 이미지입니다.

    1. 왜 XSS 취약점이 아직도 자주 나오냐고요?

    생각보다 단순하거든요. 입력값을 받는 위치는 많은데, 출력할 때 문맥(context)을 제대로 구분하지 않아서 그렇습니다. 쉽게 말해, 사용자가 넣은 값을 화면에 다시 보여줄 때 HTML 문맥인지, 속성(attribute) 문맥인지, JavaScript 문맥인지 구분해야 하는데, 여기서 하나만 어긋나도 XSS 공격으로 이어질 수 있습니다.

    예를 들어 검색창, 게시판 제목, 프로필 소개, 관리자 메모, 에러 메시지, 심지어 URL 파라미터를 그대로 렌더링하는 부분까지 전부 후보입니다. 실제로 써보니까 프론트엔드 프레임워크가 자동 이스케이프를 해주는 경우도 많지만, 예외가 있거든요. 템플릿 엔진에서 raw 출력이 들어가거나, 서버에서 만든 HTML 조각을 그대로 내려보내거나, 클라이언트에서 innerHTML 같은 식으로 꽂아 넣는 순간 위험해집니다.

    • Reflected XSS(반사형 XSS): 요청에 포함된 값이 응답에 바로 반영될 때 주로 발생합니다.
    • Stored XSS(저장형 XSS): DB 등에 저장된 악성 스크립트가 다른 사용자 화면에서 실행됩니다.
    • DOM-based XSS(DOM 기반 XSS): 서버 응답보다 브라우저 내 스크립트 처리 과정에서 발생합니다.

    여기서 중요한 포인트! OWASP Top 10을 공부할 때 XSS를 그냥 “스크립트 막기” 정도로만 보면 놓치는 게 많습니다. 핵심은 신뢰하지 않는 입력(untrusted input)을 어떤 문맥에서 출력하는가입니다.

    2. OWASP ZAP이 정확히 뭘 해주나요?

    OWASP ZAP(Zed Attack Proxy, 웹 애플리케이션 보안 점검 프록시)은 브라우저와 서버 사이에 끼워 넣어서 요청/응답을 관찰하고, 취약점 탐지까지 도와주는 보안 감사 도구라고 보시면 돼요. Burp Suite를 먼저 접해보신 분이라면 비슷한 감각으로 이해하셔도 됩니다. 저는 처음엔 프록시 개념이 낯설었는데, 한 번 구조를 잡고 나니까 디버깅할 때 엄청 편하더라고요.

    OWASP ZAP에서 XSS를 볼 때 주로 쓰는 기능은 아래 네 가지입니다.

    기능 설명 언제 유용한가
    Intercept 요청을 중간에서 잡아 수정 파라미터 변조, 재현 테스트
    Spider 사이트 구조를 수집 숨은 엔드포인트 탐색
    Passive Scan 트래픽을 보며 수동 분석 부담 적은 초기 점검
    Active Scan 공격 페이로드를 보내며 점검 실제 취약점 확인

    운영 서비스에 바로 Active Scan(액티브 스캔, 실제 공격성 요청 포함)을 넣는 건 조심하셔야 합니다. 테스트 환경이나 스테이징에서 먼저 하시는 게 맞습니다. 이건 진짜 중요해요. 의도치 않게 데이터가 바뀌거나, WAF(Web Application Firewall, 웹 애플리케이션 방화벽) 경보를 울릴 수도 있거든요.

    3. 실전 환경 구성: 브라우저 프록시부터 맞춥니다

    제가 보통 하는 첫 단계는 단순해요. 브라우저 트래픽이 ZAP을 지나가도록 프록시를 맞추고, 대상 애플리케이션을 평소처럼 직접 조작해봅니다. 자동 스캔부터 돌리기보다, 먼저 정상 흐름을 타보는 게 훨씬 좋습니다. 어느 파라미터가 어디서 어떻게 반영되는지 감이 오거든요.

    1. OWASP ZAP을 실행합니다.
    2. 브라우저 프록시를 ZAP 로컬 프록시에 맞춥니다.
    3. 테스트 대상 URL을 브라우저로 직접 접속합니다.
    4. 로그인, 검색, 게시글 작성, 프로필 수정처럼 입력이 있는 화면을 우선 탐색합니다.
    5. ZAP의 Sites 트리와 History에서 요청/응답을 확인합니다.
    # 예시: 로컬 개발 서버 실행
    npm run dev
    
    # 또는 Python 예시
    python app.py

    애플리케이션이 로컬에서 돌아간다면 더 편해요. 수정하고, 다시 요청 보내고, 결과 확인하는 루프가 빠르거든요. 저는 이 단계에서 먼저 검색창, 에러 메시지, 상세 화면 제목 같은 곳을 체크합니다. 왜냐하면 reflected XSS가 생각보다 이런 데서 잘 보이기 때문입니다.

    브라우저 프록시 설정과 ZAP 사이트 트리 화면 개요

    브라우저 프록시가 ZAP을 거쳐 사이트 트리와 요청 히스토리가 채워지는 흐름을 설명하는 이미지입니다.

    4. 제가 실제로 많이 보는 XSS 의심 패턴

    처음엔 이게 뭔가 싶었는데, 몇 번 보다 보면 냄새가 나는 지점이 있어요. 예를 들어 아래 같은 코드요.

    from flask import Flask, request, render_template_string
    
    app = Flask(__name__)
    
    @app.route('/search')
    def search():
        q = request.args.get('q', '')
        return render_template_string("""
            <h1>Search</h1>
            <div>Result for: %s</div>
        """ % q)

    이 코드는 사용자 입력값 <code>q를 그대로 HTML에 끼워 넣고 있어요. 겉보기에는 단순하지만, 여기서 <script> 같은 값이 들어가면 브라우저가 코드로 해석할 수 있습니다. 실제 서비스 코드에서는 이렇게 대놓고 문자열 포매팅하는 경우는 드물어도, 템플릿 조합이나 HTML fragment 생성 로직에서 비슷한 문제가 숨어 있더라고요.

    프런트엔드에서도 마찬가지입니다.

    const keyword = new URLSearchParams(location.search).get('q');
    document.getElementById('result').innerHTML = 'Result: ' + keyword;

    이건 전형적인 DOM 기반 XSS 위험 패턴이거든요. textContent를 써야 할 자리에 innerHTML을 쓰고 있어요. 저도 예전에 테스트용 관리자 페이지에서 이런 식으로 빨리 만들었다가 나중에 보안 점검에서 걸린 적이 있습니다. 기능은 잘 되는데 보안은 별개더라고요.

    5. OWASP ZAP으로 XSS 재현하는 방법

    이제 본격적으로 OWASP ZAP XSS 진단을 해보겠습니다. 제가 많이 쓰는 흐름은 Passive Scan으로 냄새를 맡고, 직접 재현 페이로드를 넣어본 다음, 필요할 때만 Active Scan을 거는 방식이거든요.

    1. 입력값이 들어가는 요청을 하나 고릅니다.
    2. ZAP History에서 해당 요청을 찾습니다.
    3. Request를 열고 파라미터 값을 XSS 테스트 문자열로 바꿉니다.
    4. 응답 HTML과 브라우저 렌더링 결과를 같이 봅니다.
    5. ZAP Alerts에서 XSS 관련 경고를 확인합니다.
    # 예시 페이로드
    <script>alert(1)</script>
    
    # 속성 문맥 점검용 예시
    "><img src=x onerror=alert(1)>
    
    # 단순 반영 여부 확인용 예시
    zap-test-123

    여기서 팁 하나. 무조건 공격적인 문자열부터 넣지 말고, 먼저 zap-test-123 같은 무해한 문자열이 응답 어디에 반영되는지 확인하세요. 그 다음 문맥에 맞는 페이로드로 좁혀가는 게 좋아요. 이 과정을 건너뛰면 “경고는 떴는데 왜 안 터지지?” 하면서 시간 많이 써요. 제가 딱 그랬거든요.

    만약 로그인 후 화면만 점검해야 한다면, 세션 쿠키가 살아 있는 상태로 브라우저를 통해 먼저 정상 요청을 만들고, 그 요청을 재사용하는 방식이 편합니다. ZAP이 세션 흐름을 계속 보고 있으니까 재현이 빨라집니다.

    ZAP에서 요청 파라미터를 수정해 XSS 페이로드를 넣는 장면

    히스토리에서 요청을 선택하고 파라미터를 수정해 테스트 문자열을 넣는 과정을 보여주는 이미지입니다.

    6. 취약점이 확인되면 어디를 고쳐야 하나요?

    이 부분이 가장 중요하거든요. XSS는 단순 필터링으로 끝내면 다시 터집니다. 제가 직접 해보니, 결국은 출력 시점의 문맥별 인코딩(output encoding)과 안전한 DOM 조작으로 가야 재발이 줄었어요.

    상황 문제 패턴 권장 대응
    HTML 본문 출력 사용자 문자열 직접 삽입 템플릿 자동 이스케이프 사용
    HTML 속성값 따옴표 포함 값 삽입 속성 문맥 인코딩 적용
    JavaScript 내 문자열 스크립트에 직접 변수 주입 JSON 직렬화 후 안전하게 전달
    DOM 조작 innerHTML 사용 textContent, createElement 사용

    서버 템플릿 기준으로는 raw 출력이나 safe 플래그 남용을 줄이는 게 먼저고요. 프런트엔드 기준으로는 사용자 입력을 HTML로 만들지 않는 습관이 정말 중요하거든요.

    from flask import Flask, request, render_template
    
    app = Flask(__name__)
    
    @app.route('/search')
    def search():
        q = request.args.get('q', '')
        return render_template('search.html', q=q)
    <h1>Search</h1>
    <div>Result for: {{ q }}</div>

    프런트엔드는 이렇게 바꾸는 편이 안전해요.

    const keyword = new URLSearchParams(location.search).get('q') || '';
    document.getElementById('result').textContent = 'Result: ' + keyword;

    추가로 Content Security Policy(CSP, 콘텐츠 보안 정책)도 같이 검토해보세요. CSP는 근본 해결책을 대체하진 못하지만, 인라인 스크립트 실행을 제한하는 보조 방어선으로 꽤 도움이 돼요.

    Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'

    7. ⚠️ 제가 실제로 겪은 트러블슈팅 포인트

    여기서부터가 진짜 현업 감각이 들어가는 부분입니다. ZAP 경고가 떴다고 무조건 exploitable(실제 악용 가능)한 건 아니고, 반대로 화면에서 팝업이 안 떴다고 안전한 것도 아니에요.

    • 경고는 떴는데 브라우저에서 실행이 안 됨
      응답에 값이 반영되긴 했지만 HTML 이스케이프가 일부 적용돼 있거나, CSP 때문에 실행이 막힌 경우였어요. 이때는 응답 원문과 개발자도구 DOM을 같이 봐야 합니다.
    • 테스트 문자열이 보이지 않음
      서버가 아니라 클라이언트 렌더링 구간에서 값이 처리되는 경우가 있어요. DOM 기반 XSS 가능성을 봐야 해요. ZAP만 보지 말고 브라우저 콘솔과 Elements 탭도 같이 보세요.
    • WAF가 중간에서 차단함
      공격 문자열이 차단돼서 정상 재현이 안 되는 경우가 있었어요. 이럴 땐 무해한 문자열로 먼저 반영 위치를 찾고, 스테이징 환경에서 재확인하는 게 좋습니다.
    • 입력값 검증만 추가했는데 다른 화면에서 다시 발생
      한 화면만 막고 공통 렌더링 유틸은 그대로 둔 경우였어요. 출력 레이어 전체를 봐야 해요.

    저는 특히 “검색 페이지 하나만 고치면 끝”이라고 생각했다가, 같은 값을 상세 팝오버와 관리자 로그 화면이 또 다른 방식으로 출력해서 다시 잡힌 적이 있어요. 이런 게 은근 많거든요. 그래서 한 번 취약점이 나오면, 입력의 출발점이 아니라 출력의 종착점 전체를 추적하는 게 맞습니다.

    트러블슈팅 체크리스트

    1. 반영 위치가 서버 응답인지, DOM 렌더링 이후인지 구분합니다.
    2. 문자열이 어떤 문맥에 들어가는지 확인합니다.
    3. 템플릿 자동 이스케이프가 비활성화된 구간이 있는지 봅니다.
    4. innerHTML, dangerouslySetInnerHTML 같은 위험 API 사용 여부를 찾습니다.
    5. CSP 적용 상태와 브라우저 차단 로그를 확인합니다.
    6. ZAP Alert를 근거로 동일 패턴이 다른 화면에도 있는지 확장 점검합니다.

    8. 수정 후 검증은 이렇게 했습니다

    수정이 끝났다고 바로 안심하시면 안 돼요. 보안 이슈는 회귀(regression, 수정 후 다른 경로에서 재발) 확인이 중요하거든요. 저는 보통 세 단계로 확인합니다.

    1. 처음 문제를 재현했던 동일 요청을 다시 보냅니다.
    2. 유사한 입력 필드에도 같은 테스트를 반복합니다.
    3. ZAP Passive/Active Scan 결과에서 관련 경고 변화를 봅니다.
    # 애플리케이션 로그 확인 예시
    tail -f app.log
    
    # 프런트 정적 분석/테스트 예시
    npm test

    이때 “팝업이 안 뜬다”만 보지 말고, 응답 HTML이 안전하게 이스케이프되는지, DOM에 텍스트로 들어가는지까지 확인하는 게 좋아요. 가능하면 보안 관련 회귀 테스트도 같이 넣어두세요. 예를 들어 검색 결과에 특수문자가 들어왔을 때 HTML이 아니라 텍스트로 렌더링되는지 확인하는 테스트요.

    검증이 끝나면 ZAP Alerts에서 해당 경고가 사라졌는지, 또는 위험도가 낮아졌는지 확인해요. 환경에 따라 경고가 남을 수도 있는데, 그 경우는 false positive(오탐)인지 근거를 남겨두는 게 좋습니다.

    취약점 수정 전후를 비교하고, 검증 포인트를 시각적으로 정리한 이미지입니다.

    9. 정리: OWASP ZAP XSS 점검은 도구보다 해석이 중요합니다

    오늘 정리한 내용을 한 줄로 줄이면 이렇습니다. OWASP ZAP XSS 진단은 시작점일 뿐이고, 진짜 실력 차이는 경고를 어떻게 읽고 코드까지 추적해서 고치느냐에서 나타나요. 저도 처음엔 도구가 다 잡아줄 줄 알았는데, 실제로 써보니까 결국 사람이 문맥을 이해해야 하더라고요.

    특히 XSS 공격은 눈에 안 띄게 숨어 있다가, 관리자 화면이나 내부 도구처럼 방심한 곳에서 터지는 경우가 많아요. 그래서 개발 편의성 때문에 넣어둔 raw 렌더링, 빠른 화면 조합용 HTML 삽입 같은 부분을 다시 보는 습관이 중요합니다. 혹시 이런 경험 있으신가요? 기능은 멀쩡한데 보안 점검에서 갑자기 발목 잡는 순간이요. 그럴 때 당황하지 마시고, 입력값 반영 위치부터 차근차근 좁혀가시면 됩니다.

    다음 글에서는 CSP 설정을 조금 더 깊게 다뤄보거나, DOM 기반 XSS를 브라우저 개발자도구 중심으로 추적하는 방법도 정리해볼 예정입니다. 이전 글에서 다뤘던 리버스 프록시와 로그 분석 글이 있다면 같이 보셔도 흐름 잡는 데 도움이 됩니다.

    XSS 진단부터 수정, 재검증까지 요약한 인포그래픽

    OWASP ZAP XSS 진단, 수정, 재검증 흐름을 한 장으로 요약한 마무리 이미지입니다.

    빠른 요약

    • OWASP ZAP은 프록시 기반으로 요청/응답과 취약점 탐지를 도와주는 강력한 도구입니다.
    • XSS는 입력 필터보다 출력 문맥별 인코딩과 안전한 DOM 조작이 핵심이에요.
    • 경고 자체보다 실제 반영 위치와 실행 가능성을 해석하는 과정이 더 중요합니다.
    • 수정 후에는 동일 요청 재현, 유사 화면 확장 점검, 회귀 테스트까지 확인해야 해요.

    자주 묻는 질문

    Q. OWASP ZAP 경고가 뜨면 바로 취약점이라고 봐도 되나요?
    A. 아니에요. 오탐 가능성이 있어서 응답 원문, DOM 반영 방식, 브라우저 동작을 같이 확인해야 해요.

    Q. 입력값에서 script 태그만 막으면 충분한가요?
    A. 보통은 부족해요. 속성 문맥, JavaScript 문맥, DOM 조작까지 고려해야 해서 출력 시점 방어가 더 중요합니다.

    Q. 운영 환경에서도 Active Scan을 돌려도 되나요?
    A. 권장하지 않아요. 스테이징이나 테스트 환경에서 먼저 검증하는 편이 훨씬 안전합니다.

  • [보안] SQL 인젝션 방어: 모의 해킹 사례로 본 실전 대응

    [보안] SQL 인젝션 방어: 모의 해킹 사례로 본 실전 대응

    [보안] SQL 인젝션 방어: 모의 해킹 사례로 본 실전 대응

    웹 개발 13년, 인프라 운영 경험으로 느낀 건 SQL 인젝션 방어가 아직도 현장에서 가장 흔한 취약점이라는 점입니다. 특히 급하게 만든 관리자 페이지, 레거시 PHP 코드, 내부 업무용 도구에서 툭 튀어나오더라고요. 대단한 제로데이보다 기본기 부족에서 더 많이 터집니다. 이번 글은 제가 홈랩과 사내 교육 환경에서 직접 재현했던 모의 해킹 사례를 바탕으로, SQL 인젝션이 어떻게 발견되고 어떤 순서로 막아야 하는지 정리한 글입니다. 침투 테스트를 준비하는 분이나 OWASP Top 10을 실무 관점에서 이해하고 싶은 분께 도움이 될 거에요.

    처음엔 “요즘 누가 저렇게 단순한 실수를 해?” 싶었는데, 막상 로그를 따라가 보니 사소한 문자열 결합 하나가 인증 우회(Authentication Bypass)로 이어지더라고요. 여기서 중요한 포인트! SQL 인젝션 방어는 웹방화벽 하나 올린다고 끝나는 문제가 아니라, 코드 작성 습관과 검증 절차까지 같이 바뀌어야 합니다.

    SQL 인젝션 방어 아키텍처와 공격 흐름을 보여주는 다이어그램

    사용자 입력이 웹 서버, 애플리케이션, 데이터베이스로 흐르면서 어느 지점에서 검증과 바인딩이 필요한지 보여주는 개요 이미지입니다.

    1. 왜 아직도 SQL 인젝션이 계속 나올까요?

    쉽게 말해 SQL 인젝션(SQL Injection)은 사용자 입력값이 쿼리 문자열에 그대로 섞여 들어가는 문제입니다. 공격자는 입력창에 단순한 아이디나 검색어 대신 SQL 구문 일부를 넣고, 애플리케이션이 그걸 그대로 실행하게 만드는 거죠.

    제가 실무에서 자주 봤던 패턴은 이런 식입니다.

    • 로그인 페이지에서 아이디와 비밀번호를 문자열로 이어붙임
    • 검색 페이지에서 LIKE 조건을 직접 조합함
    • 정렬 파라미터(sort, order by)를 화이트리스트 없이 그대로 사용함
    • 에러 메시지를 자세히 노출해서 DB 구조를 유추하게 만듦

    OWASP Top 10에서도 인젝션 계열 취약점은 계속 중요하게 다뤄집니다. 이름이나 분류는 시기에 따라 조금씩 달라져도, 본질은 같습니다. 신뢰하면 안 되는 입력을 신뢰했다. 결국 여기로 돌아오더라고요.

    2. 개념부터 짚고 가보겠습니다

    2-1. 취약한 쿼리는 어떻게 만들어질까요?

    예를 들어 로그인 로직이 아래처럼 되어 있다고 해보겠습니다. 이런 코드는 교육용 예제에서 정말 많이 보지만, 레거시 시스템에서도 형태만 조금 바뀐 채 남아 있는 경우가 있습니다.

    <?php
    $id = $_POST['id'];
    $pw = $_POST['pw'];
    
    $sql = "SELECT * FROM users WHERE id = '$id' AND pw = '$pw'";
    $result = mysqli_query($conn, $sql);
    ?>

    문제는 <code>$id나 $pw에 작은따옴표와 조건식을 넣을 수 있다는 점입니다. 예를 들어 공격자가 아래처럼 넣으면요.

    id: admin' --
    pw: anything

    실제 쿼리는 대략 이런 식으로 바뀝니다.

    SELECT * FROM users WHERE id = 'admin' --' AND pw = 'anything'

    -- 뒤는 주석 처리되니 비밀번호 검증이 사실상 사라집니다. 처음 보면 “이게 진짜 먹나?” 싶은데, 조건식과 주석 규칙을 이해하면 왜 위험한지 바로 보입니다.

    2-2. SQL 인젝션 방어의 핵심은 뭘까요?

    제가 여러 번 삽질해보고 정리한 기준은 딱 네 가지입니다.

    1. Prepared Statement(준비된 문장) 또는 Parameterized Query(매개변수화 쿼리) 사용
    2. Input Validation(입력 검증)과 Allowlist(허용 목록) 적용
    3. Least Privilege(최소 권한) 원칙으로 DB 계정 분리
    4. Logging & Monitoring(로깅과 모니터링)으로 이상 징후 탐지

    많은 분이 1번만 기억하시는데, 실제로 운영해보면 3번과 4번이 사고 범위를 줄이는 데 정말 큽니다.

    3. 모의 해킹 사례: 로그인 우회 취약점 재현

    이 사례는 실제 고객 시스템이 아니라, 제가 홈랩에서 만든 교육용 환경입니다. 민감한 정보 없이 재현 가능한 형태로 구성했고, 침투 테스트 관점에서 어떤 흐름으로 점검하는지 보여드리려고 준비했습니다.

    3-1. 테스트 환경

    구성 요소 역할 설명
    웹 애플리케이션 로그인 기능 제공 의도적으로 취약한 문자열 결합 방식 사용
    MariaDB/MySQL 계열 DB 사용자 정보 저장 일반적인 LAMP 계열 환경과 유사하게 구성
    Burp Suite Community 요청 가로채기 프록시(Proxy) 용도로 활용
    sqlmap 자동화 점검 보조 수동 확인 후 보조적으로 사용

    실제로 써보니까 자동화 도구부터 돌리는 것보다, 먼저 요청 구조를 사람이 이해하는 게 훨씬 중요하더라고요. 자동화는 빨리 찾게 해주지만, 원인 분석까지 대신해주진 않습니다.

    3-2. 1단계: 요청 패턴 확인

    1. 브라우저 프록시를 Burp로 설정합니다.
    2. 로그인 페이지에 임의 계정으로 요청을 보냅니다.
    3. POST Body에 id, pw가 어떻게 전달되는지 확인합니다.
    4. 응답 코드와 에러 메시지 차이를 비교합니다.
    POST /login HTTP/1.1
    Host: lab.local
    Content-Type: application/x-www-form-urlencoded
    
    id=test&pw=test1234

    제가 가장 먼저 보는 건 세 가지입니다. 입력값 필터링이 있는지, 실패 응답이 모두 같은지, 그리고 데이터베이스 에러가 노출되는지요. 이 세 개만 봐도 절반은 감이 옵니다.

    3-3. 2단계: 수동 페이로드로 반응 확인

    다음으로는 아주 기본적인 문자열부터 넣어봅니다.

    '\n"\n' OR '1'='1

    만약 작은따옴표 하나만 넣었는데 500 에러가 나거나 SQL 문법 오류가 보이면, 일단 입력값이 쿼리에 직접 반영되고 있을 가능성이 큽니다. 여기서 너무 세게 들어가지 말고, 반응 차이를 보는 수준에서 멈추는 게 좋습니다. 모의 해킹은 증명이 목적이지, 실제 데이터 훼손이 목적이 아니거든요.

    모의 해킹 사례에서 로그인 요청을 분석하는 SQL 인젝션 방어 테스트 장면

    로그인 요청의 파라미터 구조와 응답 차이를 분석하는 장면을 보여주는 이미지입니다.

    3-4. 3단계: 로그인 우회 확인

    교육용 환경에서는 아래 같은 페이로드로 우회가 재현됐습니다.

    id=admin' -- \npw=anything

    또는 조건식을 분기시키는 방식도 가능합니다.

    id=' OR '1'='1' -- \npw=anything

    제가 직접 해보니 재미있는 지점이 하나 있었습니다. 개발자는 “비밀번호도 같이 검사하니까 괜찮다”라고 생각했는데, 실제로는 아이디 필드 하나만으로도 뒤쪽 조건이 다 무력화되더라고요. 현장에서 정말 흔한 착각입니다.

    4. 취약한 코드와 안전한 코드 비교

    4-1. 문자열 결합 방식의 문제

    # vulnerable example
    username = request.form['username']
    password = request.form['password']
    query = f"SELECT id FROM users WHERE username = '{username}' AND password = '{password}'"
    cursor.execute(query)

    이 코드는 보기엔 간단하지만, 입력값에 쿼리 제어권을 넘겨주는 셈입니다. 저도 예전에 사내 도구를 빠르게 붙이다가 비슷한 유혹을 받은 적이 있는데요. “내부망인데 뭐” 하고 넘어가면 나중에 더 크게 돌아옵니다. 내부 시스템도 피싱이나 계정 탈취 이후엔 공격 표적이 되거든요.

    4-2. Prepared Statement로 수정

    # safer example
    username = request.form['username']
    password = request.form['password']
    query = "SELECT id FROM users WHERE username = %s AND password = %s"
    cursor.execute(query, (username, password))

    핵심은 DB 드라이버가 데이터와 쿼리 구조를 분리해서 처리하게 만드는 겁니다. 즉, 사용자 입력은 더 이상 SQL 구문이 아니라 단순 값으로만 취급됩니다. SQL 인젝션 방어의 가장 기본이자 가장 강력한 방법이죠.

    4-3. 정렬 조건은 따로 처리해야 합니다

    여기서 많이 놓치는 부분이 있습니다. 테이블명, 컬럼명, 정렬 방향 같은 건 파라미터 바인딩으로 막기 어려운 경우가 있거든요. 이런 값은 반드시 허용 목록으로 제한해야 합니다.

    allowed_sort = {"created_at", "username", "last_login"}
    sort = request.args.get("sort", "created_at")
    if sort not in allowed_sort:
        sort = "created_at"
    query = f"SELECT id, username, created_at FROM users ORDER BY {sort} DESC"

    처음엔 번거로워 보여도, 실제 운영을 생각하면 훨씬 낫습니다. 허용된 선택지만 열어두는 방식이 결국 유지보수도 편하더라고요.

    5. SQL 인젝션 방어 전략을 운영 관점에서 정리해보겠습니다

    5-1. 코드 레벨 방어

    • Prepared Statement를 기본값처럼 사용합니다.
    • ORM(Object-Relational Mapping)을 쓰더라도 Raw Query(직접 쿼리) 사용 지점을 점검합니다.
    • 에러 메시지에 SQL 구문이나 테이블명을 노출하지 않습니다.
    • 입력 길이 제한과 형식 검증을 함께 둡니다.

    5-2. DB 레벨 방어

    • 애플리케이션 계정에 DROP, ALTER 같은 불필요 권한을 주지 않습니다.
    • 읽기 전용 서비스는 Read-Only 계정을 분리합니다.
    • 운영 계정과 마이그레이션(Migration) 계정을 분리합니다.

    5-3. 인프라 레벨 방어

    • WAF(Web Application Firewall)는 보조 수단으로 둡니다.
    • 리버스 프록시(Reverse Proxy) 또는 API Gateway에서 비정상 패턴을 로깅합니다.
    • DB 접근 경로를 내부 네트워크로 제한합니다.
    • 감사 로그(Audit Log)와 애플리케이션 로그를 함께 봅니다.
    취약한 쿼리와 안전한 SQL 인젝션 방어 코드를 비교한 이미지

    문자열 결합 방식과 매개변수 바인딩 방식의 차이를 한눈에 보여주는 비교 이미지입니다.

    5-4. 어떤 방어가 어디까지 막아주나요?

    방어 방법 막아주는 범위 한계
    Prepared Statement 값 기반 입력을 통한 인젝션 차단 동적 컬럼/정렬명은 별도 검증 필요
    입력 검증 비정상 값 조기 차단 검증만으로 완전 방어는 어려움
    최소 권한 DB 계정 침해 시 피해 범위 축소 취약점 자체가 사라지진 않음
    WAF 알려진 패턴 필터링 우회 가능, 오탐 가능
    로그 모니터링 사후 탐지와 대응 속도 개선 예방책은 아님

    6. ⚠️ 실제로 많이 겪는 문제와 트러블슈팅

    제가 현업과 홈랩에서 자주 봤던 삽질 포인트를 정리해봤습니다. 저도 처음엔 헷갈렸는데, 이 부분만 잡아도 재발이 크게 줄었습니다.

    6-1. 이스케이프만 하면 된다고 믿는 경우

    문자 이스케이프(Escape)는 보조 수단입니다. DB 설정, 문자셋, 드라이버 차이 때문에 예상과 다르게 동작할 수 있습니다. 예전에 레거시 앱 하나에서 수동 이스케이프 함수만 믿고 있었는데, 특정 입력에서 필터 우회가 가능해서 결국 전부 Prepared Statement로 갈아탔습니다. 그때 좀 삽질했습니다 ㅎㅎ

    6-2. ORM 쓰니까 안전하다고 생각하는 경우

    ORM을 써도 Raw SQL을 직접 붙이면 똑같이 위험합니다. 특히 관리자 검색 기능이나 통계 쿼리에서 예외적으로 직접 작성한 SQL이 문제를 만들더라고요.

    6-3. 에러 페이지가 너무 친절한 경우

    운영 중 디버그 모드가 켜져 있으면 테이블명, 컬럼명, 스택 트레이스(Stack Trace)까지 노출됩니다. 공격자 입장에서는 힌트 모음집입니다. 운영 환경에서는 일반화된 오류 메시지만 보여주고, 상세 내용은 내부 로그로만 남겨야 합니다.

    6-4. 탐지 룰이 없어서 시도 자체를 모르는 경우

    이상하게도 방어 코드는 넣어두고 로그는 안 보는 팀이 꽤 있습니다. 아래처럼 웹 로그에서 단순 패턴이라도 먼저 잡아두면 도움이 됩니다.

    grep -Ei "union select|or 1=1|-- |sleep\(|benchmark\(" /var/log/nginx/access.log

    물론 이 방식은 완전하지 않습니다. 하지만 초기 탐지나 교육용 시연에는 꽤 직관적입니다. 이후엔 SIEM(Security Information and Event Management)이나 중앙 로그 수집으로 확장하면 좋고요.

    7. 검증: 수정 후 어떻게 확인할까요?

    수정했다고 바로 끝내면 안 됩니다. SQL 인젝션 방어는 재현 테스트와 회귀 테스트(Regression Test)까지 묶어야 진짜 닫힌 겁니다.

    1. 기존 정상 로그인/조회 기능이 그대로 동작하는지 확인합니다.
    2. 이전에 먹히던 페이로드가 더 이상 동작하지 않는지 확인합니다.
    3. 에러 메시지가 일반화되어 있는지 확인합니다.
    4. DB 계정 권한이 최소화되었는지 확인합니다.
    5. 로그에 공격 시도가 남는지 확인합니다.
    sqlmap -u "http://lab.local/login" \
      --data="id=test&pw=test1234" \
      --batch \
      --risk=1 \
      --level=1

    중요한 건 자동화 도구 결과를 맹신하지 않는 겁니다. 제가 직접 해보니, 방어가 잘 되어 있어도 앱 특성 때문에 의심 결과가 나오는 경우가 있었고요. 반대로 일부 동적 쿼리는 사람이 맥락을 봐야 진짜 위험성을 알 수 있었습니다.

    SQL 인젝션 방어 적용 전후 결과와 로그 분석을 보여주는 대시보드

    취약점 재현 실패, 로그 탐지 성공, 권한 축소 적용 여부를 시각적으로 확인하는 결과 이미지입니다.

    7-1. 간단 체크리스트

    • ✅ 모든 사용자 입력이 바인딩 처리되는가
    • ✅ 동적 정렬/필터 값은 허용 목록으로 제한되는가
    • ✅ 운영 에러 페이지에 SQL 정보가 노출되지 않는가
    • ✅ 앱 DB 계정이 과도한 권한을 갖고 있지 않은가
    • ✅ 침투 테스트 후 재검증 기록이 남아 있는가

    8. 정리와 다음 단계

    이번 모의 해킹 사례에서 핵심은 단순합니다. SQL 인젝션은 화려한 공격이 아니라, 입력을 쿼리 구조와 섞어버린 개발 습관에서 시작됩니다. 그리고 SQL 인젝션 방어는 Prepared Statement 하나로 출발하지만, 최소 권한, 에러 처리, 로깅, 재검증까지 이어져야 제대로 닫힙니다.

    혹시 지금 운영 중인 내부 도구가 있다면, 로그인과 검색 기능부터 먼저 보세요. 거기가 가장 자주 터집니다. 저도 처음엔 “이 정도는 괜찮겠지” 했다가 나중에 테스트 로그 보고 식은땀 흘린 적이 있거든요. 드디어 됐다! 싶은 순간은 보통 재현이 안 될 때가 아니라, 왜 안 되는지 근거까지 설명할 수 있을 때 오더라고요.

    다음 글에서는 XSS(Cross-Site Scripting)와 CSRF(Cross-Site Request Forgery)를 묶어서 다룰 예정입니다. 이전 글의 로그 분석 편도 같이 보시면 흐름이 더 잘 잡히실 겁니다.

    Prepared Statement, 입력 검증, 최소 권한, 모니터링을 한 장으로 요약한 마무리 이미지입니다.

    FAQ

    Q1. WAF만 있으면 SQL 인젝션 방어가 충분한가요?

    아닙니다. WAF는 우회 가능성이 있고, 애플리케이션 코드 수준 문제를 근본적으로 해결하지 못합니다. 보조 장비로 봐야 합니다.

    Q2. 내부망 서비스도 꼭 점검해야 하나요?

    네. 내부망 서비스는 방심하기 쉬워서 오히려 취약한 경우가 많습니다. 계정 탈취나 VPN 접속 이후 lateral movement의 출발점이 되기도 합니다.

    Q3. 침투 테스트는 어디부터 시작하면 좋을까요?

    로그인, 검색, 정렬, 필터, 다운로드 파라미터처럼 사용자 입력이 DB 질의와 연결되는 화면부터 시작하시면 됩니다. 이 순서가 실패 확률이 낮습니다.