프로그래밍/Python

🚨 Django 프로덕션에서 DEBUG=True? 당신의 서비스가 위험합니다!

Tiboong 2025. 9. 11. 17:41
728x90
반응형

"Django 개발자라면 한 번은 저지르는 치명적 실수"

새벽 2시, 급하게 서비스를 배포하고 나서 안도의 한숨을 쉬었는데... 며칠 후 보안팀에서 연락이 왔습니다.

"사용자 개인정보가 웹페이지에 그대로 노출되고 있습니다."

원인은 단 한 줄이었습니다.

# settings.py
DEBUG = True  # 이 한 줄이 모든 것을 망쳤습니다

 

27년간 수많은 프로젝트를 봐오면서, 이 실수로 인한 보안 사고를 정말 많이 봤습니다. 오늘은 Django에서 DEBUG=True가 얼마나 위험한지, 실제 사례와 함께 알아보겠습니다.

 

💥 실제로 어떤 정보가 노출될까요?

케이스 1: 데이터베이스 정보 유출

개발자가 작성한 코드:

# views.py
def user_profile(request, user_id):
    try:
        user = User.objects.get(id=user_id)
        return render(request, 'profile.html', {'user': user})
    except User.DoesNotExist:
        # 존재하지 않는 사용자 ID로 접근했을 때
        raise Http404("User not found")

 

DEBUG=True일 때 사용자가 보는 화면:

DoesNotExist at /user/99999/

User matching query does not exist.

Request Method: GET
Request URL: http://yoursite.com/user/99999/
Django Version: 4.2.1
Exception Type: DoesNotExist
Exception Value: User matching query does not exist.

Traceback (most recent call last):
  File "/app/views.py", line 15, in user_profile
    user = User.objects.get(id=user_id)
    
Settings:
DEBUG = True
SECRET_KEY = 'django-insecure-abcd1234...'  # 🚨 SECRET_KEY 노출!
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'production_db',
        'USER': 'db_user',
        'PASSWORD': 'super_secret_password123',  # 🚨 DB 비밀번호 노출!
        'HOST': 'db.internal.company.com',
        'PORT': '5432',
    }
}

INSTALLED_APPS = [...]
MIDDLEWARE = [...]
...

 

공격자가 얻는 정보:

  • 🎯 데이터베이스 접속 정보 (호스트, 사용자명, 비밀번호)
  • 🎯 Django SECRET_KEY
  • 🎯 프로젝트 구조와 파일 경로
  • 🎯 설치된 패키지 목록
  • 🎯 내부 서버 IP 주소

 

케이스 2: API 키와 비밀 정보 노출

# settings.py에 하드코딩된 API 키들
AWS_ACCESS_KEY_ID = 'AKIAIOSFODNN7EXAMPLE'
AWS_SECRET_ACCESS_KEY = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
STRIPE_SECRET_KEY = 'sk_live_abcdef1234567890'
SENDGRID_API_KEY = 'SG.abcdef123456'
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = 'your_google_oauth_secret'

에러 페이지에서 모든 설정값이 그대로 노출됩니다!

 

케이스 3: 사용자 개인정보 노출

# 실제 있었던 사례
def payment_process(request):
    try:
        payment_data = {
            'user_email': request.user.email,
            'credit_card': request.POST.get('card_number'),
            'amount': request.POST.get('amount'),
            'user_ip': request.META['REMOTE_ADDR']
        }
        process_payment(payment_data)
    except Exception as e:
        # DEBUG=True에서는 payment_data가 그대로 노출됨
        raise Exception(f"Payment failed: {payment_data}")

결과: 신용카드 번호, 이메일, IP 주소가 에러 페이지에 그대로 표시

 

🔍 DEBUG=True가 노출하는 정보들

1. Django 설정 정보 전체

# settings.py의 모든 변수가 노출됩니다
ALLOWED_HOSTS = ['internal-server.company.com']
INTERNAL_IPS = ['10.0.0.1', '192.168.1.100']
ADMINS = [('John Doe', 'john@company.com')]
EMAIL_HOST_PASSWORD = 'email_secret_password'

2. 서버 환경 정보

Request information:
META: {
    'SERVER_NAME': 'production-web-01',
    'HTTP_HOST': 'api.yourcompany.com',
    'PATH_INFO': '/api/users/',
    'QUERY_STRING': 'token=secret_token_123',
    'HTTP_AUTHORIZATION': 'Bearer eyJ0eXAiOiJKV1QiLCJhbGc...',
    ...
}

3. 코드 구조와 비즈니스 로직

# 파일 경로와 함수명이 모두 노출
File "/app/services/payment_service.py", line 45, in process_premium_subscription
File "/app/models/user.py", line 123, in upgrade_to_premium
File "/app/utils/billing.py", line 67, in calculate_discount

 

🛡️ 올바른 설정 방법

1. 환경별 설정 분리

방법 1: 환경변수 사용

# settings.py
import os
from django.core.exceptions import ImproperlyConfigured

def get_env_variable(var_name, default=None):
    try:
        return os.environ[var_name]
    except KeyError:
        if default is not None:
            return default
        error_msg = f"Set the {var_name} environment variable"
        raise ImproperlyConfigured(error_msg)

# 환경에 따라 자동으로 설정
DEBUG = get_env_variable('DEBUG', 'False').lower() == 'true'
SECRET_KEY = get_env_variable('SECRET_KEY')

# 데이터베이스 설정도 환경변수로
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': get_env_variable('DB_NAME'),
        'USER': get_env_variable('DB_USER'),
        'PASSWORD': get_env_variable('DB_PASSWORD'),
        'HOST': get_env_variable('DB_HOST'),
        'PORT': get_env_variable('DB_PORT', '5432'),
    }
}

 

방법 2: 설정 파일 분리

# settings/
├── __init__.py
├── base.py        # 공통 설정
├── development.py # 개발 환경
├── staging.py     # 스테이징 환경
└── production.py  # 프로덕션 환경

# settings/production.py
from .base import *

DEBUG = False
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']

# 보안 관련 설정
SECURE_SSL_REDIRECT = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True

# 로그 설정
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'verbose': {
            'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
            'style': '{',
        },
    },
    'handlers': {
        'file': {
            'level': 'INFO',
            'class': 'logging.FileHandler',
            'filename': '/var/log/django/django.log',
            'formatter': 'verbose',
        },
    },
    'root': {
        'handlers': ['file'],
        'level': 'INFO',
    },
}

 

2. 프로덕션 체크리스트

# settings/production.py 필수 보안 설정

# 1. DEBUG 비활성화
DEBUG = False

# 2. ALLOWED_HOSTS 명시적 설정
ALLOWED_HOSTS = ['yourdomain.com']

# 3. 보안 헤더 설정
SECURE_SSL_REDIRECT = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

# 4. 정적 파일 설정
STATIC_ROOT = '/var/www/static/'
MEDIA_ROOT = '/var/www/media/'

# 5. 에러 페이지 커스터마이징
ADMINS = [('Admin', 'admin@yourdomain.com')]
MANAGERS = ADMINS

# 6. 캐시 설정
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.redis.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/1',
    }
}

 

3. 배포 자동화로 실수 방지

# .github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    
    - name: Check Django Settings
      run: |
        # DEBUG=True 체크
        if grep -q "DEBUG = True" */settings/*.py; then
          echo "❌ ERROR: DEBUG=True found in settings!"
          exit 1
        fi
        
        # SECRET_KEY 하드코딩 체크
        if grep -q "SECRET_KEY = ['\"]" */settings/*.py; then
          echo "❌ ERROR: Hardcoded SECRET_KEY found!"
          exit 1
        fi
        
        echo "✅ Security check passed"
    
    - name: Deploy
      run: |
        echo "Deploying with DJANGO_SETTINGS_MODULE=settings.production"

 

🚨 실제 사고 사례

사례 1: 스타트업 A사

  • 상황: 급하게 배포하면서 DEBUG=True 그대로 두고 배포
  • 결과: Google 검색에서 에러 페이지가 인덱싱되어 DB 접속 정보 노출
  • 피해: 데이터베이스 해킹, 고객 정보 10만 건 유출
  • 교훈: 배포 전 자동화된 체크 시스템 필요

사례 2: 중견 기업 B사

  • 상황: 스테이징 환경 설정을 프로덕션에 그대로 적용
  • 결과: AWS 키가 에러 페이지에 노출되어 크립토마이닝에 악용
  • 피해: 월 AWS 비용 2000만원 → 8000만원으로 급증
  • 교훈: 환경별 설정 완전 분리 필수

 

🛠️ 응급 대응 방법

이미 DEBUG=True로 배포했다면?

1. 즉시 조치

# 1. 긴급 설정 변경
export DJANGO_SETTINGS_MODULE=settings.production
export DEBUG=False

# 2. 서버 재시작
sudo systemctl restart gunicorn
sudo systemctl restart nginx

# 3. 로그 확인
tail -f /var/log/nginx/access.log | grep "500\|400"

 

2. 보안 점검

# management/commands/security_check.py
from django.core.management.base import BaseCommand
from django.conf import settings

class Command(BaseCommand):
    def handle(self, *args, **options):
        issues = []
        
        if settings.DEBUG:
            issues.append("🚨 DEBUG=True in production!")
        
        if 'django-insecure' in settings.SECRET_KEY:
            issues.append("🚨 Default SECRET_KEY detected!")
        
        if not settings.ALLOWED_HOSTS:
            issues.append("🚨 ALLOWED_HOSTS not configured!")
        
        if issues:
            self.stdout.write(self.style.ERROR('\n'.join(issues)))
        else:
            self.stdout.write(self.style.SUCCESS('✅ Security check passed'))

 

3. 피해 최소화

  • 노출된 SECRET_KEY 즉시 변경
  • 데이터베이스 비밀번호 변경
  • API 키 재발급
  • 보안 로그 점검

 

💡 프로 팁: 실수를 방지하는 개발 문화

1. 코드 리뷰 체크리스트

## 배포 전 보안 체크리스트
- [ ] DEBUG=False 확인
- [ ] SECRET_KEY 환경변수 처리
- [ ] 하드코딩된 비밀번호 없음
- [ ] ALLOWED_HOSTS 적절히 설정
- [ ] 보안 헤더 설정 완료

 

2. 배포 스크립트에 안전장치

#!/bin/bash
# deploy.sh

# 프로덕션 배포 전 필수 체크
echo "🔍 Checking production settings..."

if [ "$DJANGO_SETTINGS_MODULE" != "settings.production" ]; then
    echo "❌ DJANGO_SETTINGS_MODULE must be 'settings.production'"
    exit 1
fi

if [ "$DEBUG" = "True" ]; then
    echo "❌ DEBUG must be False in production"
    exit 1
fi

echo "✅ All checks passed. Deploying..."

 

3. 모니터링 알림 설정

# 프로덕션에서 500 에러 발생 시 즉시 알림
LOGGING = {
    'handlers': {
        'mail_admins': {
            'level': 'ERROR',
            'class': 'django.utils.log.AdminEmailHandler',
            'include_html': False,  # HTML 포함하지 않음 (보안)
        },
    },
    'loggers': {
        'django': {
            'handlers': ['mail_admins'],
            'level': 'ERROR',
            'propagate': True,
        },
    },
}

 

🎯 결론: 작은 실수가 만드는 큰 사고

DEBUG=True 하나로 인해:

  • 💸 금전적 피해: AWS 크레딧 도용, 법적 배상금
  • 📰 신뢰도 손상: 언론 보도, 고객 신뢰 실추
  • ⚖️ 법적 책임: GDPR, 개인정보보호법 위반
  • 👥 인적 피해: 개발자 책임 추궁, 팀 해체

기억하세요:

  • ✅ 환경별 설정 완전 분리
  • ✅ 배포 자동화에 보안 체크 포함
  • ✅ 정기적인 보안 감사
  • ✅ 팀원 교육과 코드 리뷰

 

💬 여전히 Django 보안이 걱정되시나요?

Django 프로젝트의 보안 설정이 제대로 되어 있는지 확신이 서지 않으시나요?

27년 경력의 시니어 개발자가 여러분의 Django 프로젝트를 직접 점검해드립니다.

  • 🔍 보안 취약점 전체 점검
  • 긴급 보안 패치 적용
  • 📋 프로덕션 배포 가이드 제작
  • 👨‍💻 팀 교육 및 모범 사례 전수

➡️ 전문가 보안 컨설팅 받기

"작은 실수가 큰 사고가 되기 전에, 지금 점검하세요!"

728x90
반응형