Skip to content
Published on

애플리케이션 보안 엔지니어링 가이드 — OWASP Top 10, 인증, 암호화, DevSecOps

Authors

애플리케이션 보안 엔지니어링 가이드

소프트웨어가 비즈니스의 핵심이 된 시대, 보안은 선택이 아니라 생존의 문제입니다. 이 가이드는 OWASP Top 10부터 DevSecOps 파이프라인까지 애플리케이션 보안의 전 범위를 다룹니다.


1. 왜 보안이 중요한가

데이터 유출의 현실

IBM의 "Cost of a Data Breach Report"에 따르면, 2024년 기준 데이터 유출 한 건의 평균 비용은 약 488만 달러에 달합니다. 유출 사고의 주요 원인은 다음과 같습니다.

원인비율평균 비용
피싱 공격16%476만 달러
도난/유출된 인증정보15%463만 달러
클라우드 설정 오류12%414만 달러
제로데이 취약점10%518만 달러

개발자의 보안 책임

보안은 더 이상 보안팀만의 책임이 아닙니다. "Shift Left" 패러다임에 따라, 개발 초기 단계에서 보안을 내재화해야 합니다.

  • 코드 작성 단계에서 취약점을 방지하는 것이 사후 패치보다 6~100배 저렴합니다
  • SDLC(소프트웨어 개발 수명 주기) 전체에서 보안을 고려해야 합니다
  • 보안은 기능(feature)이 아니라 품질(quality)의 일부입니다

보안의 3대 원칙: CIA Triad

  • Confidentiality (기밀성): 인가된 사용자만 데이터에 접근 가능
  • Integrity (무결성): 데이터가 무단으로 변경되지 않음을 보장
  • Availability (가용성): 필요할 때 시스템과 데이터에 접근 가능

2. OWASP Top 10 (2021)

OWASP Top 10은 웹 애플리케이션에서 가장 치명적인 10가지 보안 취약점을 정리한 것입니다. 각각의 취약점과 방어법을 살펴봅니다.

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

가장 흔한 취약점입니다. 인가되지 않은 사용자가 다른 사용자의 데이터에 접근하거나 관리자 기능을 수행할 수 있습니다.

공격 시나리오: URL 파라미터를 변경하여 다른 사용자의 주문 내역을 조회

방어법:

# Flask에서 접근 제어 데코레이터
from functools import wraps
from flask import abort, g

def require_role(role):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            if not g.current_user or g.current_user.role != role:
                abort(403)
            return f(*args, **kwargs)
        return decorated_function
    return decorator

@app.route('/admin/users')
@require_role('admin')
def admin_users():
    return get_all_users()
  • 기본적으로 모든 접근을 거부하고, 명시적으로 허용하는 화이트리스트 방식을 사용합니다
  • 서버 사이드에서 반드시 권한을 검증합니다
  • IDOR(Insecure Direct Object References) 방지를 위해 UUID를 사용합니다

A02: Cryptographic Failures (암호화 실패)

민감 데이터가 평문으로 전송되거나 약한 암호화로 보호되는 경우입니다.

방어법:

  • 전송 중 데이터: TLS 1.3 사용
  • 저장 데이터: AES-256-GCM으로 암호화
  • 비밀번호: bcrypt 또는 Argon2로 해싱
  • MD5, SHA-1 같은 취약한 알고리즘을 사용하지 않습니다

A03: Injection (인젝션)

SQL, NoSQL, OS 명령어, LDAP 등의 인젝션 공격입니다.

취약한 코드:

# 절대 이렇게 하면 안 됩니다 - SQL Injection에 취약
query = "SELECT * FROM users WHERE id = " + user_input

안전한 코드:

# 파라미터화 쿼리 사용
cursor.execute("SELECT * FROM users WHERE id = %s", (user_input,))

# ORM 사용 (SQLAlchemy)
user = User.query.filter_by(id=user_input).first()

A04: Insecure Design (안전하지 않은 설계)

설계 단계에서의 보안 결함입니다. 코드 수준이 아닌 아키텍처 수준의 문제입니다.

방어법:

  • Threat Modeling 수행 (STRIDE, PASTA 방법론)
  • 보안 설계 원칙 적용 (최소 권한, 심층 방어, 실패 안전)
  • 비즈니스 로직에 대한 보안 시나리오 테스트

A05: Security Misconfiguration (보안 설정 오류)

기본 계정 미변경, 불필요한 기능 활성화, 디버그 모드 운영 환경 노출 등이 해당됩니다.

체크리스트:

  • 기본 비밀번호를 변경했는가
  • 불필요한 포트와 서비스를 비활성화했는가
  • 에러 메시지에 스택 트레이스가 노출되지 않는가
  • 보안 헤더(X-Frame-Options, X-Content-Type-Options)가 설정되었는가

A06: Vulnerable and Outdated Components (취약하고 오래된 컴포넌트)

알려진 취약점이 있는 라이브러리나 프레임워크를 사용하는 경우입니다.

방어법:

# npm 보안 감사
npm audit

# Python 의존성 검사
pip-audit

# Go 취약점 검사
govulncheck ./...

A07: Identification and Authentication Failures (인증 실패)

약한 비밀번호 정책, 세션 관리 미흡, 브루트포스 공격 방치 등이 해당됩니다. 자세한 내용은 3장에서 다룹니다.

A08: Software and Data Integrity Failures (무결성 실패)

CI/CD 파이프라인, 소프트웨어 업데이트, 직렬화에서의 무결성 검증 부족입니다.

방어법:

  • 디지털 서명으로 소프트웨어 무결성 검증
  • SBOM(Software Bill of Materials) 관리
  • CI/CD 파이프라인 보안 (코드 서명, 접근 제어)

A09: Security Logging and Monitoring Failures (로깅 및 모니터링 실패)

보안 이벤트를 기록하지 않거나, 기록하더라도 모니터링하지 않는 경우입니다.

필수 로깅 항목:

import logging
import json
from datetime import datetime, timezone

security_logger = logging.getLogger('security')

def log_security_event(event_type, user_id, details):
    event = {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "event_type": event_type,
        "user_id": user_id,
        "ip_address": request.remote_addr,
        "user_agent": request.headers.get('User-Agent'),
        "details": details
    }
    security_logger.warning(json.dumps(event))

# 사용 예시
log_security_event("LOGIN_FAILED", "user123", "Invalid password attempt 3")
log_security_event("PRIVILEGE_ESCALATION", "user456", "Attempted admin access")

A10: Server-Side Request Forgery (SSRF)

서버가 공격자가 지정한 URL로 요청을 보내도록 유도하는 공격입니다.

방어법:

  • 허용된 URL 화이트리스트 적용
  • 내부 네트워크 IP 대역(10.x, 172.16.x, 192.168.x)으로의 요청 차단
  • DNS Rebinding 방어

3. 인증(Authentication)

인증은 사용자가 자신이 주장하는 사람인지 확인하는 과정입니다.

비밀번호 해싱

비밀번호는 절대 평문이나 가역 암호화로 저장하면 안 됩니다.

# Argon2 - 현재 가장 권장되는 해싱 알고리즘
from argon2 import PasswordHasher

ph = PasswordHasher(
    time_cost=3,       # 반복 횟수
    memory_cost=65536,  # 64MB 메모리
    parallelism=4,      # 병렬 스레드 수
    hash_len=32,        # 해시 길이
    salt_len=16         # 솔트 길이
)

# 해싱
hashed = ph.hash("user_password")
# 결과 예시: $argon2id$v=19$m=65536,t=3,p=4$...

# 검증
try:
    ph.verify(hashed, "user_password")
    print("비밀번호 일치")
except Exception:
    print("비밀번호 불일치")
# bcrypt - 널리 사용되는 대안
import bcrypt

password = b"user_password"
salt = bcrypt.gensalt(rounds=12)
hashed = bcrypt.hashpw(password, salt)

# 검증
if bcrypt.checkpw(password, hashed):
    print("비밀번호 일치")

다중 인증(MFA)

비밀번호만으로는 충분하지 않습니다. 최소 두 가지 인증 요소를 조합합니다.

인증 요소설명예시
지식 (Something you know)사용자가 알고 있는 정보비밀번호, PIN
소유 (Something you have)사용자가 소유한 물건스마트폰, 보안 키
생체 (Something you are)사용자의 신체 특징지문, 얼굴 인식
# TOTP (Time-based One-Time Password) 구현
import pyotp

# 비밀키 생성
secret = pyotp.random_base32()

# QR 코드 URI 생성
totp_uri = pyotp.totp.TOTP(secret).provisioning_uri(
    name="user@example.com",
    issuer_name="MyApp"
)

# 검증
totp = pyotp.TOTP(secret)
is_valid = totp.verify("123456")  # 사용자 입력 코드

패스키(WebAuthn/FIDO2)

비밀번호 없는 미래를 위한 인증 표준입니다.

  • 공개키 암호화 기반으로 피싱에 강합니다
  • 생체 인증 또는 PIN으로 로컬 인증 후, 서버에는 공개키만 전달됩니다
  • Apple, Google, Microsoft 모두 지원합니다
// 패스키 등록 (브라우저 측)
const credential = await navigator.credentials.create({
  publicKey: {
    challenge: serverChallenge,
    rp: { name: "MyApp", id: "myapp.com" },
    user: {
      id: userId,
      name: "user@example.com",
      displayName: "사용자"
    },
    pubKeyCredParams: [
      { alg: -7, type: "public-key" },   // ES256
      { alg: -257, type: "public-key" }  // RS256
    ],
    authenticatorSelection: {
      authenticatorAttachment: "platform",
      residentKey: "required",
      userVerification: "required"
    }
  }
});

4. 인가(Authorization)

인가는 인증된 사용자가 어떤 리소스에 접근할 수 있는지 결정하는 과정입니다.

RBAC (역할 기반 접근 제어)

가장 보편적인 인가 모델입니다. 사용자에게 역할을 부여하고, 역할에 권한을 매핑합니다.

# RBAC 구현 예시
PERMISSIONS = {
    "admin": ["read", "write", "delete", "manage_users"],
    "editor": ["read", "write"],
    "viewer": ["read"],
}

def check_permission(user_role, required_permission):
    role_permissions = PERMISSIONS.get(user_role, [])
    return required_permission in role_permissions

# 사용
if check_permission(current_user.role, "delete"):
    delete_resource(resource_id)
else:
    raise PermissionError("삭제 권한이 없습니다")

ABAC (속성 기반 접근 제어)

사용자 속성, 리소스 속성, 환경 속성을 기반으로 세밀한 접근 제어를 수행합니다.

# ABAC 정책 예시
def evaluate_policy(user, resource, action, environment):
    # 근무 시간에만 접근 허용
    if environment["hour"] < 9 or environment["hour"] > 18:
        return False

    # 같은 부서의 문서만 접근 가능
    if user["department"] != resource["department"]:
        return False

    # 매니저는 모든 작업 가능, 일반 직원은 읽기만
    if action == "write" and user["level"] < 3:
        return False

    return True

OAuth 2.0 스코프

OAuth 2.0에서 스코프는 클라이언트가 접근할 수 있는 리소스의 범위를 제한합니다.

# OAuth 2.0 스코프 정의 예시
scopes:
  read:profile: "사용자 프로필 읽기"
  write:profile: "사용자 프로필 수정"
  read:orders: "주문 내역 조회"
  admin:users: "사용자 관리 (관리자 전용)"

JWT 보안 모범 사례

JWT는 편리하지만, 잘못 사용하면 심각한 보안 문제가 발생합니다.

import jwt
from datetime import datetime, timedelta, timezone

# JWT 생성 (RS256 권장)
def create_token(user_id, role):
    payload = {
        "sub": user_id,
        "role": role,
        "iat": datetime.now(timezone.utc),
        "exp": datetime.now(timezone.utc) + timedelta(minutes=15),
        "iss": "myapp.com",
        "aud": "myapp.com"
    }
    return jwt.encode(payload, PRIVATE_KEY, algorithm="RS256")

# JWT 검증
def verify_token(token):
    try:
        payload = jwt.decode(
            token,
            PUBLIC_KEY,
            algorithms=["RS256"],  # 알고리즘 명시적 지정 (none 공격 방지)
            issuer="myapp.com",
            audience="myapp.com"
        )
        return payload
    except jwt.ExpiredSignatureError:
        raise AuthError("토큰이 만료되었습니다")
    except jwt.InvalidTokenError:
        raise AuthError("유효하지 않은 토큰입니다")

JWT 보안 체크리스트:

  • HS256 대신 RS256 사용 (비대칭 서명)
  • 만료 시간(exp)을 짧게 설정 (15분 이내)
  • Refresh Token은 서버 사이드에서 관리
  • alg: none 공격을 방지하기 위해 알고리즘을 명시적으로 검증

5. 암호화 실전

대칭 암호화: AES-256-GCM

동일한 키로 암호화와 복호화를 수행합니다. 데이터 저장 시 주로 사용됩니다.

from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os

# 키 생성 (256비트)
key = AESGCM.generate_key(bit_length=256)

# 암호화
aesgcm = AESGCM(key)
nonce = os.urandom(12)  # 96비트 논스
plaintext = b"Sensitive data here"
aad = b"additional authenticated data"  # 추가 인증 데이터

ciphertext = aesgcm.encrypt(nonce, plaintext, aad)

# 복호화
decrypted = aesgcm.decrypt(nonce, ciphertext, aad)

비대칭 암호화: RSA와 ECDSA

from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import hashes, serialization

# ECDSA 키 쌍 생성
private_key = ec.generate_private_key(ec.SECP256R1())
public_key = private_key.public_key()

# 서명
from cryptography.hazmat.primitives.asymmetric import utils
signature = private_key.sign(
    b"message to sign",
    ec.ECDSA(hashes.SHA256())
)

# 검증
public_key.verify(
    signature,
    b"message to sign",
    ec.ECDSA(hashes.SHA256())
)

TLS 설정 모범 사례

# Nginx TLS 설정
server {
    listen 443 ssl http2;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers on;

    # HSTS 헤더
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

    # OCSP Stapling
    ssl_stapling on;
    ssl_stapling_verify on;
}

키 관리와 HSM

  • 키를 소스 코드에 하드코딩하면 안 됩니다
  • AWS KMS, Google Cloud KMS, Azure Key Vault 같은 관리형 서비스를 사용합니다
  • HSM(Hardware Security Module)은 키를 하드웨어 내부에서만 처리하여 추출을 방지합니다
  • 키 로테이션 정책을 수립하고 자동화합니다

6. 시큐어 코딩

입력 검증

모든 외부 입력은 신뢰할 수 없습니다.

import re
from pydantic import BaseModel, validator, constr

class UserInput(BaseModel):
    username: constr(min_length=3, max_length=30, pattern=r'^[a-zA-Z0-9_]+$')
    email: str
    age: int

    @validator('email')
    def validate_email(cls, v):
        pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
        if not re.match(pattern, v):
            raise ValueError('유효하지 않은 이메일 형식입니다')
        return v

    @validator('age')
    def validate_age(cls, v):
        if v < 0 or v > 150:
            raise ValueError('유효하지 않은 나이입니다')
        return v

출력 인코딩 (XSS 방지)

# HTML 출력 시 이스케이프
from markupsafe import escape

user_input = '<script>alert("XSS")</script>'
safe_output = escape(user_input)
# 결과: &lt;script&gt;alert("XSS")&lt;/script&gt;

파라미터화 쿼리

# SQLAlchemy ORM 사용
from sqlalchemy import text

# 안전한 방법
result = db.session.execute(
    text("SELECT * FROM users WHERE email = :email"),
    {"email": user_email}
)

CORS 설정

# Flask-CORS 설정
from flask_cors import CORS

CORS(app, resources={
    r"/api/*": {
        "origins": ["https://myapp.com", "https://admin.myapp.com"],
        "methods": ["GET", "POST", "PUT", "DELETE"],
        "allow_headers": ["Content-Type", "Authorization"],
        "max_age": 86400
    }
})

CSP (Content Security Policy)

# CSP 헤더 설정
@app.after_request
def set_csp(response):
    response.headers['Content-Security-Policy'] = (
        "default-src 'self'; "
        "script-src 'self' 'nonce-abc123'; "
        "style-src 'self' 'unsafe-inline'; "
        "img-src 'self' data: https:; "
        "connect-src 'self' https://api.myapp.com; "
        "frame-ancestors 'none'; "
        "base-uri 'self'"
    )
    return response

7. API 보안

Rate Limiting

# Flask-Limiter 사용
from flask_limiter import Limiter

limiter = Limiter(
    app,
    default_limits=["200 per day", "50 per hour"]
)

@app.route("/api/login", methods=["POST"])
@limiter.limit("5 per minute")
def login():
    # 로그인 로직
    pass

@app.route("/api/data")
@limiter.limit("100 per hour")
def get_data():
    # 데이터 조회 로직
    pass

API Key 관리

  • API Key는 헤더로 전달합니다 (URL 파라미터에 넣지 않습니다)
  • 키별 권한 범위(scope)를 제한합니다
  • 키 발급/폐기 이력을 관리합니다
  • 사용량을 모니터링하고 이상 패턴을 감지합니다

mTLS (상호 TLS 인증)

서버와 클라이언트 양쪽 모두 인증서를 검증하는 방식입니다.

# Kubernetes Ingress에서 mTLS 설정 예시
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    nginx.ingress.kubernetes.io/auth-tls-verify-client: "on"
    nginx.ingress.kubernetes.io/auth-tls-secret: "default/ca-secret"
spec:
  tls:
    - hosts:
        - api.myapp.com
      secretName: tls-secret

HMAC 서명

요청의 무결성과 인증을 동시에 보장합니다.

import hmac
import hashlib
import time

def create_hmac_signature(secret_key, method, path, body, timestamp):
    message = f"{method}\n{path}\n{timestamp}\n{body}"
    signature = hmac.new(
        secret_key.encode(),
        message.encode(),
        hashlib.sha256
    ).hexdigest()
    return signature

# 검증 (타임스탬프 체크 포함)
def verify_request(secret_key, method, path, body, timestamp, signature):
    # 5분 이내의 요청만 허용 (Replay 공격 방지)
    if abs(time.time() - float(timestamp)) > 300:
        return False

    expected = create_hmac_signature(secret_key, method, path, body, timestamp)
    return hmac.compare_digest(expected, signature)

8. 컨테이너 보안

이미지 스캔

# Trivy로 컨테이너 이미지 취약점 스캔
trivy image myapp:latest

# 심각도별 필터링
trivy image --severity HIGH,CRITICAL myapp:latest

# CI/CD에서 사용 (취약점 발견 시 빌드 실패)
trivy image --exit-code 1 --severity CRITICAL myapp:latest

최소 권한 Dockerfile

# 멀티 스테이지 빌드
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

# 프로덕션 이미지
FROM node:20-alpine
RUN addgroup -g 1001 -S appgroup && \
    adduser -S appuser -u 1001 -G appgroup
WORKDIR /app
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules

# 루트가 아닌 사용자로 실행
USER appuser
EXPOSE 3000
CMD ["node", "dist/server.js"]

Rootless 컨테이너

  • 루트 권한 없이 컨테이너를 실행합니다
  • securityContext에서 runAsNonRoot, readOnlyRootFilesystem을 설정합니다
  • 불필요한 Linux Capabilities를 제거합니다
# Kubernetes Pod Security Context
apiVersion: v1
kind: Pod
metadata:
  name: secure-app
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1001
    fsGroup: 1001
  containers:
    - name: app
      image: myapp:latest
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop:
            - ALL

OPA/Gatekeeper

Kubernetes 클러스터의 정책을 코드로 관리합니다.

# 특권 컨테이너 차단 정책
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sPSPPrivilegedContainer
metadata:
  name: deny-privileged
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
    excludedNamespaces:
      - kube-system

9. DevSecOps 파이프라인

전체 파이프라인 개요

DevSecOps는 개발(Dev), 보안(Sec), 운영(Ops)을 통합하는 접근법입니다.

단계도구목적
코드 작성IDE 보안 플러그인실시간 취약점 감지
커밋Pre-commit hooks비밀키 유출 방지
빌드SAST정적 코드 분석
테스트DAST동적 보안 테스트
의존성SCA서드파티 라이브러리 취약점
배포이미지 스캔컨테이너 취약점
운영RASP, WAF런타임 보안

SAST (Static Application Security Testing)

# GitHub Actions에서 SonarQube 실행
name: Security Scan
on: [push, pull_request]

jobs:
  sonarqube:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: SonarQube Scan
        uses: sonarsource/sonarqube-scan-action@master
        env:
          SONAR_TOKEN: SONAR_TOKEN_PLACEHOLDER
          SONAR_HOST_URL: SONAR_HOST_PLACEHOLDER

DAST (Dynamic Application Security Testing)

# OWASP ZAP을 사용한 동적 보안 스캔
docker run -t zaproxy/zap-stable zap-baseline.py \
  -t https://myapp.com \
  -r report.html

SCA (Software Composition Analysis)

# Dependabot 설정 (.github/dependabot.yml)
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10
  - package-ecosystem: "pip"
    directory: "/"
    schedule:
      interval: "weekly"

SBOM (Software Bill of Materials)

# Syft로 SBOM 생성
syft myapp:latest -o spdx-json > sbom.json

# Grype로 SBOM 기반 취약점 스캔
grype sbom:sbom.json

Pre-commit에서 비밀키 감지

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/Yelp/detect-secrets
    rev: v1.4.0
    hooks:
      - id: detect-secrets
        args: ['--baseline', '.secrets.baseline']
  - repo: https://github.com/zricethezav/gitleaks
    rev: v8.18.0
    hooks:
      - id: gitleaks

10. 침투 테스트 기초

모의해킹 방법론

OWASP Testing Guide 기반의 테스트 단계:

  1. 정보 수집 (Reconnaissance): 대상 시스템의 기술 스택, 네트워크 구조 파악
  2. 매핑 (Mapping): 공격 표면(Attack Surface) 식별
  3. 취약점 발견 (Discovery): 자동화 도구와 수동 테스트 병행
  4. 취약점 검증 (Exploitation): 발견된 취약점의 실제 위험도 검증
  5. 보고 (Reporting): 기술적 상세 내용과 비즈니스 영향도 보고

유용한 도구들

도구용도
Burp Suite웹 프록시, 취약점 스캐너
Nmap네트워크 스캔
sqlmapSQL Injection 자동화
Nuclei템플릿 기반 취약점 스캔
Metasploit침투 테스트 프레임워크

버그 바운티 프로그램

자사 서비스의 취약점을 외부 보안 연구자가 발견하도록 보상하는 프로그램입니다.

  • HackerOne: 세계 최대의 버그 바운티 플랫폼
  • Bugcrowd: 크라우드소싱 보안 테스트
  • 자체 운영: 명확한 범위(scope)와 보상 기준 설정이 중요합니다

CTF (Capture The Flag) 추천

보안 실력을 키울 수 있는 CTF 플랫폼입니다.

  • OverTheWire: 기초부터 고급까지 단계별 워게임
  • Hack The Box: 실전형 침투 테스트 환경
  • PicoCTF: 초보자에게 적합한 교육용 CTF
  • PortSwigger Web Security Academy: 웹 보안 무료 학습 플랫폼

11. 인시던트 대응

보안 사고 대응 절차 (NIST SP 800-61)

1단계: 준비 (Preparation)

  • 인시던트 대응 팀(IRT) 구성
  • 대응 계획 수립 및 훈련
  • 연락 체계와 에스컬레이션 절차 정의

2단계: 탐지 및 분석 (Detection and Analysis)

  • SIEM(Security Information and Event Management)으로 이상 탐지
  • 로그 분석 및 IoC(Indicators of Compromise) 식별
  • 사고의 범위와 영향도 파악

3단계: 억제, 근절, 복구 (Containment, Eradication, Recovery)

# 비상 대응 예시: 유출된 API 키 무효화
# 1. 영향받은 키 즉시 폐기
aws iam delete-access-key --user-name compromised-user --access-key-id AKIA...

# 2. 관련 세션 모두 무효화
aws iam put-user-policy --user-name compromised-user \
  --policy-name DenyAll --policy-document file://deny-all.json

# 3. 새 인증 정보 발급 후 안전하게 배포

4단계: 사후 활동 (Post-Incident Activity)

  • 사후 보고서(Post-mortem) 작성
  • 타임라인 재구성
  • 근본 원인 분석 (Root Cause Analysis)
  • 재발 방지 대책 수립 및 이행

디지털 포렌식 기초

  • 증거 보전의 원칙: 원본을 훼손하지 않고 사본으로 분석합니다
  • Chain of Custody: 증거의 취급 이력을 문서화합니다
  • 메모리 덤프, 디스크 이미징, 네트워크 트래픽 캡처 등 수집 방법을 이해합니다
  • 주요 도구: Volatility (메모리 분석), Autopsy (디스크 분석), Wireshark (네트워크 분석)

보안 사고 보고서 구조

좋은 보안 사고 보고서는 다음 항목을 포함합니다.

  1. 사고 개요: 발생 일시, 유형, 영향 범위
  2. 타임라인: 최초 침투부터 발견까지의 시간순 기록
  3. 기술적 분석: 공격 벡터, 악용된 취약점, 공격자의 행위
  4. 영향 분석: 유출된 데이터의 종류와 규모, 비즈니스 영향
  5. 대응 조치: 취한 조치와 효과
  6. 재발 방지: 단기/장기 개선 사항
  7. 교훈: 대응 과정에서의 개선점

마무리

애플리케이션 보안은 단일 도구나 프로세스가 아닌, 문화이자 여정입니다. 핵심 원칙을 정리합니다.

  1. 심층 방어 (Defense in Depth): 하나의 보안 계층이 실패해도 다른 계층이 방어합니다
  2. 최소 권한 (Least Privilege): 필요한 최소한의 권한만 부여합니다
  3. 실패 안전 (Fail Secure): 시스템 오류 시 보안이 유지되는 방향으로 설계합니다
  4. 제로 트러스트 (Zero Trust): 네트워크 위치에 관계없이 모든 요청을 검증합니다
  5. 보안은 문화: 모든 팀원이 보안 의식을 가지고, 지속적으로 학습합니다

보안 위협은 끊임없이 진화합니다. 이 가이드를 출발점으로 삼아 지속적인 학습과 실무 적용을 병행하시기 바랍니다.

퀴즈: 애플리케이션 보안 지식 점검

Q1. OWASP Top 10 (2021)에서 1위를 차지한 취약점은 무엇인가요?

A: Broken Access Control (접근 제어 실패). 인가되지 않은 사용자가 다른 사용자의 데이터에 접근하거나 관리자 기능을 수행할 수 있는 취약점입니다.

Q2. 비밀번호 해싱에 MD5를 사용하면 안 되는 이유는?

A: MD5는 속도가 너무 빠르기 때문에 무차별 대입 공격(Brute-force)에 취약합니다. bcrypt나 Argon2처럼 의도적으로 느리게 설계된 알고리즘을 사용해야 합니다.

Q3. JWT에서 alg: none 공격을 방지하려면 어떻게 해야 하나요?

A: 토큰 검증 시 허용할 알고리즘을 명시적으로 지정해야 합니다. 라이브러리의 기본 설정에 의존하지 않고 algorithms=["RS256"]처럼 구체적으로 지정합니다.

Q4. SAST와 DAST의 차이점은 무엇인가요?

A: SAST(Static)는 소스 코드를 실행 없이 분석하여 취약점을 찾고, DAST(Dynamic)는 실행 중인 애플리케이션을 대상으로 외부에서 공격을 시뮬레이션합니다. 둘 다 상호 보완적으로 사용해야 합니다.

Q5. SSRF 공격을 방어하는 가장 효과적인 방법은?

A: 허용된 URL의 화이트리스트를 적용하고, 내부 네트워크 IP 대역(10.x, 172.16.x, 192.168.x, 127.0.0.1)으로의 요청을 차단합니다. DNS Rebinding 공격도 함께 방어해야 합니다.