본문으로 건너뛰기
김신건의 로그

[Django] 보안: CSRF, XSS, SQL injection, headers

· 수정 · 📖 약 2분 · 778자/단어 #django #security #csrf #xss #hsts
django security, CSRF, XSS, SQL injection, security middleware, SECURE_HSTS

정의

Django는 OWASP Top 10 대부분을 기본 방어한다: CSRF 토큰, ORM의 SQL 인젝션 방지, 템플릿 자동 escape, 비밀번호 해싱, 세션 쿠키 보안 등. 그러나 설정 누락, safe 필터 남용, raw SQL 부주의가 흔한 위험. 배포 시 manage.py check --deploy로 점검.

CSRF (Cross-Site Request Forgery)

다른 사이트가 사용자 세션을 도용해 POST 요청을 보내는 공격.

자동 방어

MIDDLEWARE = [
    ...,
    "django.middleware.csrf.CsrfViewMiddleware",
]

POST/PUT/DELETE 요청은 CSRF 토큰 검증. 누락 시 403.

<form method="post">
    {% csrf_token %}    {# 필수 #}
    ...
</form>

AJAX/SPA

const csrfToken = document.cookie
    .split("; ")
    .find(r => r.startsWith("csrftoken="))
    ?.split("=")[1];

fetch("/api/posts/", {
    method: "POST",
    headers: {
        "X-CSRFToken": csrfToken,
        "Content-Type": "application/json",
    },
    body: JSON.stringify(data),
});

CSRF_COOKIE_HTTPONLY = False (기본)로 JS가 쿠키 읽을 수 있음.

면제

from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def webhook(request):
    ...

JWT 인증 API는 csrf_exempt 또는 별도 처리. 그러나 세션 인증 + csrf_exempt는 위험.

CSRF_TRUSTED_ORIGINS

POST 요청의 Origin/Referer 검증. HTTPS 서비스에 필수.

CSRF_TRUSTED_ORIGINS = [
    "https://example.com",
    "https://*.example.com",
]

XSS (Cross-Site Scripting)

악성 스크립트가 다른 사용자 브라우저에서 실행.

자동 방어

DTL은 변수 출력 시 자동 HTML escape.

{{ user_input }}    {# <script> &lt;script&gt; #}

safe 필터 = 무력화

{{ user_input|safe }}    {# 위험! 신뢰할 수 있는 HTML에만 #}

mark_safe() (Python) 동일.

Content Security Policy

INSTALLED_APPS += ["csp"]    # django-csp
MIDDLEWARE += ["csp.middleware.CSPMiddleware"]

CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'", "https://cdn.example.com")
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'")
CSP_IMG_SRC = ("'self'", "data:", "https:")

스크립트 출처 제한 → XSS 완화.

nonce 사용 (인라인 스크립트)

CSP_INCLUDE_NONCE_IN = ["script-src"]
<script nonce="{{ request.csp_nonce }}">...</script>

SQL Injection

ORM 사용 시 자동 방어. ? 또는 %s 플레이스홀더.

# 안전 (ORM)
Post.objects.filter(title=user_input)

# 안전 (raw with params)
Post.objects.raw("SELECT * FROM blog_post WHERE title = %s", [user_input])

# 안전 (cursor)
with connection.cursor() as c:
    c.execute("SELECT * FROM blog_post WHERE id = %s", [user_input])

위험

# WRONG
Post.objects.raw(f"SELECT * FROM blog_post WHERE title = '{user_input}'")
Post.objects.extra(where=[f"title = '{user_input}'"])

문자열 포매팅으로 SQL 만들지 말 것. 항상 placeholder.

비밀번호 보안

PASSWORD_HASHERS = [
    "django.contrib.auth.hashers.Argon2PasswordHasher",
    "django.contrib.auth.hashers.PBKDF2PasswordHasher",
    "django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
    "django.contrib.auth.hashers.ScryptPasswordHasher",
]

AUTH_PASSWORD_VALIDATORS = [
    {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
    {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", "OPTIONS": {"min_length": 12}},
    {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
    {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]

Argon2 권장 (argon2-cffi 설치 필요).

세션 / 쿠키 보안

SESSION_COOKIE_SECURE = True       # HTTPS only
SESSION_COOKIE_HTTPONLY = True     # JS 접근 차단
SESSION_COOKIE_SAMESITE = "Lax"    # CSRF 보호 강화
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_HTTPONLY = False        # JS에서 읽어야 (AJAX)
SESSION_COOKIE_AGE = 1209600        # 2주
SESSION_EXPIRE_AT_BROWSER_CLOSE = False

SameSite=Lax는 일반 GET 요청은 허용, POST는 같은 사이트만. Strict는 더 엄격하나 외부 링크에서 로그인 상태 유지 안 됨.

HTTPS 강제

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_SSL_REDIRECT = True

# HSTS
SECURE_HSTS_SECONDS = 31536000             # 1년
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True                  # hstspreload.org 등록 후

HSTS는 brower가 HTTPS만 사용하게 강제. 잘못 설정 시 사이트 접근 불가 → 단계적 적용 (짧은 시간부터).

Clickjacking

MIDDLEWARE += ["django.middleware.clickjacking.XFrameOptionsMiddleware"]
X_FRAME_OPTIONS = "DENY"    # 또는 "SAMEORIGIN"

iframe 안에 사이트가 임베드되는 걸 차단. 클릭재킹 공격 방어.

Content-Type, MIME sniffing

SECURE_CONTENT_TYPE_NOSNIFF = True    # X-Content-Type-Options: nosniff

브라우저가 MIME 추측을 막아 XSS 일부 방어.

Host header attack

ALLOWED_HOSTS = ["example.com", "www.example.com"]    # * 금지
USE_X_FORWARDED_HOST = True    # 리버스 프록시 뒤

Host header injection 차단.

파일 업로드

DATA_UPLOAD_MAX_MEMORY_SIZE = 5 * 1024 * 1024     # 5MB
FILE_UPLOAD_MAX_MEMORY_SIZE = 5 * 1024 * 1024
DATA_UPLOAD_MAX_NUMBER_FIELDS = 1000

업로드 파일 검증:

  • 확장자 화이트리스트
  • MIME 타입 검증 (python-magic)
  • 이미지는 Pillow로 재인코딩 (악성 페이로드 제거)
  • 저장 위치 격리 (실행 권한 X)
  • 파일명 sanitize (path traversal 방지)
def save_upload(f):
    ext = Path(f.name).suffix.lower()
    if ext not in {".jpg", ".png", ".pdf"}:
        raise ValueError("forbidden type")

    safe_name = uuid4().hex + ext
    path = Path("media") / safe_name
    with path.open("wb") as out:
        for chunk in f.chunks():
            out.write(chunk)

CORS

API + SPA 분리 시.

pip install django-cors-headers
INSTALLED_APPS += ["corsheaders"]
MIDDLEWARE = [
    "corsheaders.middleware.CorsMiddleware",    # 가능한 앞쪽
    ...
]

CORS_ALLOWED_ORIGINS = ["https://app.example.com"]
CORS_ALLOW_CREDENTIALS = True

CORS_ALLOW_ALL_ORIGINS = True는 개발용만. 프로덕션은 명시.

Rate limiting

# DRF
REST_FRAMEWORK = {
    "DEFAULT_THROTTLE_CLASSES": [
        "rest_framework.throttling.UserRateThrottle",
        "rest_framework.throttling.AnonRateThrottle",
    ],
    "DEFAULT_THROTTLE_RATES": {
        "anon": "100/day",
        "user": "1000/day",
    },
}

django-ratelimit 라이브러리는 일반 view에도.

from django_ratelimit.decorators import ratelimit

@ratelimit(key="ip", rate="5/m")
def login_view(request):
    ...

보안 체크 명령

python manage.py check --deploy

배포 시 권장 설정 누락 점검. 출력 예:

WARNING: SECURE_HSTS_SECONDS not set.
WARNING: SESSION_COOKIE_SECURE not set.

CI에 추가하면 실수 방지.

비밀 정보

  • SECRET_KEY: 환경변수 (절대 커밋 X)
  • DB password: 환경변수
  • API token: Secrets Manager / Vault
  • .env.gitignore
SECRET_KEY = os.environ["SECRET_KEY"]    # 누락 시 KeyError (즉시 알림)

의존성 보안

pip install pip-audit
pip-audit

CVE 검사. safety, dependabot (GitHub) 자동화.

자주 보는 함정

1. DEBUG=True 배포

stacktrace, settings 모두 노출. 절대 금지.

2. ALLOWED_HOSTS = [”*”]

Host header attack 가능.

3. SECRET_KEY 하드코딩

git에 노출되면 즉시 회전. 환경변수로.

4. mark_safe 남용

return mark_safe(f"<b>{user_input}</b>")    # XSS!

# 안전
from django.utils.html import format_html
return format_html("<b>{}</b>", user_input)

5. raw SQL with f-string

Post.objects.raw(f"SELECT * FROM blog_post WHERE id = {id}")    # SQL injection

6. 파일 업로드 검증 안 함

확장자만 보고 PHP/JS 업로드 → 서버 권한 탈취.

7. 로그에 비밀 출력

logger.info(f"User logged in: {request.POST}")    # 비밀번호 노출

자주 보는 보안 라이브러리

  • django-axes: 로그인 시도 잠금
  • django-defender: 비슷한 기능
  • django-csp: Content Security Policy
  • django-cors-headers: CORS
  • django-honeypot: 봇 함정
  • django-ratelimit: 일반 view rate limit
  • django-otp: 2FA

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기