웹 보안 완벽 가이드: XSS, CSRF, SQL Injection 방어
웹 보안 완벽 가이드
1. XSS (Cross-Site Scripting)
악의적인 스크립트를 삽입하여 실행시키는 공격입니다.
Reflected XSS
<!-- 취약한 코드 -->
<?php
$name = $_GET['name'];
echo "Hello, $name!";
?>
<!-- 공격 -->
http://example.com?name=<script>alert(document.cookie)</script>
<!-- 결과 -->
Hello, <script>alert(document.cookie)</script>!
Stored XSS
// 댓글 저장
const comment = "<script>steal_cookie()</script>";
db.save(comment);
// 댓글 표시 (모든 사용자에게 실행됨!)
document.innerHTML = db.getComments();
DOM-based XSS
<script>
// URL에서 값 가져오기
const name = location.hash.substring(1);
document.write("Hello, " + name); // 취약!
</script>
<!-- 공격 -->
http://example.com#<img src=x onerror=alert(1)>
XSS 방어
1. 입력 검증
function sanitizeInput(input) {
return input
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'")
.replace(/\//g, "/");
}
2. CSP (Content Security Policy)
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' https://trusted.com">
3. HttpOnly 쿠키
Set-Cookie: sessionId=abc123; HttpOnly; Secure
4. React/Vue (자동 이스케이핑)
// ✅ 안전 (자동 이스케이프)
<div>{userInput}</div>
// ❌ 위험
<div dangerouslySetInnerHTML={{__html: userInput}} />
2. CSRF (Cross-Site Request Forgery)
사용자가 의도하지 않은 요청을 실행시키는 공격입니다.
공격 시나리오
<!-- 공격자의 사이트 -->
<img src="https://bank.com/transfer?to=attacker&amount=1000">
<!-- 사용자가 bank.com에 로그인된 상태면
자동으로 송금 요청 실행! -->
CSRF 방어
1. CSRF 토큰
<form action="/transfer" method="POST">
<input type="hidden" name="csrf_token" value="abc123xyz">
<input name="amount" value="1000">
<button type="submit">송금</button>
</form>
# 서버 검증
if request.form['csrf_token'] != session['csrf_token']:
return "Invalid CSRF token", 403
2. SameSite 쿠키
Set-Cookie: sessionId=abc123; SameSite=Strict; Secure
# Strict: 같은 사이트만
# Lax: GET 요청은 허용
# None: 모두 허용 (Secure 필수)
3. Referer 검증
referer = request.headers.get('Referer')
if not referer or not referer.startswith('https://mysite.com'):
return "Invalid referer", 403
3. SQL Injection
SQL 쿼리에 악의적인 코드를 삽입하는 공격입니다.
공격 예시
# ❌ 취약한 코드
username = request.form['username']
password = request.form['password']
query = f"SELECT * FROM users WHERE username='{username}' AND password='{password}'"
db.execute(query)
# 공격
username: admin' --
password: anything
# 실행되는 쿼리
SELECT * FROM users WHERE username='admin' -- ' AND password='anything'
# → 비밀번호 체크 우회!
심화 공격
-- Union 기반
username: ' UNION SELECT credit_card FROM payments --
-- Time-based Blind
username: ' OR IF(1=1, SLEEP(5), 0) --
-- Boolean-based Blind
username: ' OR 1=1 -- (True)
username: ' OR 1=2 -- (False)
SQL Injection 방어
1. Prepared Statements (가장 중요!)
# ✅ 안전
cursor.execute(
"SELECT * FROM users WHERE username=? AND password=?",
(username, password)
)
# Python
cursor.execute(
"SELECT * FROM users WHERE username=%s AND password=%s",
(username, password)
)
# Node.js
db.query(
'SELECT * FROM users WHERE username=? AND password=?',
[username, password]
)
2. ORM 사용
# SQLAlchemy
user = session.query(User).filter_by(
username=username,
password=password
).first()
# Django ORM
user = User.objects.filter(
username=username,
password=password
).first()
3. 최소 권한 원칙
-- 웹 앱용 DB 계정
GRANT SELECT, INSERT, UPDATE ON app_db.* TO 'webapp'@'localhost';
-- DROP, CREATE 권한 없음
4. 입력 검증
import re
def validate_username(username):
# 영문자, 숫자, 언더스코어만 허용
if not re.match(r'^[a-zA-Z0-9_]+$', username):
raise ValueError("Invalid username")
4. 인증과 세션 관리
안전한 비밀번호 저장
import bcrypt
# 해싱 (회원가입)
password = "user_password"
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
db.save(hashed)
# 검증 (로그인)
input_password = request.form['password']
if bcrypt.checkpw(input_password.encode(), stored_hash):
login_success()
JWT (JSON Web Token)
import jwt
# 토큰 생성
payload = {
'user_id': 123,
'exp': datetime.utcnow() + timedelta(hours=1)
}
token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
# 토큰 검증
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
user_id = payload['user_id']
except jwt.ExpiredSignatureError:
return "Token expired", 401
except jwt.InvalidTokenError:
return "Invalid token", 401
세션 고정 공격 방어
# 로그인 성공 시 세션 ID 재생성
old_session_id = session.session_id
session.regenerate_id()
db.delete_session(old_session_id)
5. API 보안
Rate Limiting
from flask_limiter import Limiter
limiter = Limiter(app, key_func=get_remote_address)
@app.route('/api/login')
@limiter.limit("5 per minute")
def login():
# 1분에 5번만 허용
pass
API Key 관리
# ❌ 코드에 하드코딩
API_KEY = "abc123xyz"
# ✅ 환경 변수
import os
API_KEY = os.getenv('API_KEY')
# ✅ .env 파일 (gitignore)
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.getenv('API_KEY')
CORS 설정
from flask_cors import CORS
# ❌ 모든 출처 허용
CORS(app, origins="*")
# ✅ 특정 출처만
CORS(app, origins=["https://app.example.com"])
6. 보안 헤더
@app.after_request
def set_security_headers(response):
# XSS 방어
response.headers['Content-Security-Policy'] = "default-src 'self'"
# 클릭재킹 방어
response.headers['X-Frame-Options'] = 'DENY'
# MIME 스니핑 방지
response.headers['X-Content-Type-Options'] = 'nosniff'
# XSS 필터
response.headers['X-XSS-Protection'] = '1; mode=block'
# HTTPS 강제
response.headers['Strict-Transport-Security'] = 'max-age=31536000'
return response
7. 파일 업로드 보안
import os
from werkzeug.utils import secure_filename
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/upload', methods=['POST'])
def upload_file():
file = request.files['file']
# 확장자 검증
if not allowed_file(file.filename):
return "Invalid file type", 400
# 파일명 sanitize
filename = secure_filename(file.filename)
# 파일 크기 제한
if len(file.read()) > 10 * 1024 * 1024: # 10MB
return "File too large", 400
file.save(os.path.join(UPLOAD_FOLDER, filename))
8. 보안 체크리스트
입력 검증:
- 모든 사용자 입력 검증
- Whitelist 방식 사용
- 길이 제한
인증/인가:
- 비밀번호 해싱 (bcrypt, Argon2)
- 세션 타임아웃
- 2FA (Two-Factor Authentication)
데이터베이스:
- Prepared Statements
- 최소 권한 원칙
- 정기 백업
API:
- Rate Limiting
- API Key 암호화
- HTTPS 사용
모니터링:
- 로그 수집
- 이상 탐지
- 보안 패치 업데이트
결론
웹 보안은 다층 방어(Defense in Depth)가 핵심입니다.
핵심 원칙:
- 입력을 신뢰하지 마라
- 출력을 이스케이프하라
- 최소 권한 원칙
- 보안 업데이트
- 모니터링과 로깅