IDOR: 불안전한 직접 객체 참조 — 가장 흔한 API 취약점
IDOR의 발생 원리, URL/쿼리/본문/헤더에서의 다양한 공격 패턴, 소유자 검증과 사용자 범위 쿼리(user-scoped query)로 방어하는 방법, 자동화 탐지 기법을 다룹니다.
지난 글에서 Broken Access Control의 6가지 유형을 개괄적으로 살펴봤습니다. 그 중에서도 버그바운티에서 가장 자주 보고되고 API 보안에서 가장 흔히 발견되는 **IDOR(Insecure Direct Object Reference, 불안전한 직접 객체 참조)**를 이번 글에서 집중적으로 분석합니다.
IDOR란?
서버가 사용자 요청의 리소스 식별자(ID)를 받아 DB를 조회할 때, 그 리소스가 요청자의 것인지 검증하지 않으면 IDOR입니다. 공격자는 자신의 리소스 ID를 타인의 ID로 바꾸기만 하면 다른 사람의 데이터를 열람하거나 수정할 수 있습니다.
이름이 가리키듯 “직접 객체 참조”가 문제의 핵심입니다. 사용자가 URL이나 파라미터로 DB 레코드를 직접 지목할 수 있고, 서버가 이를 수동적으로 수용하는 구조입니다.
공격 벡터별 상세
URL 경로 파라미터:
# 계정 1337의 세부 정보
GET /api/users/1337/details
# 1337 대신 다른 ID로 변경
GET /api/users/1338/details → 타인 정보 노출?
GET /api/users/1/details → 관리자 정보?
쿼리 파라미터:
# 청구서 조회
GET /invoices?invoice_id=INV-2026-001
# 파라미터 변조
GET /invoices?invoice_id=INV-2025-001 → 작년 청구서?
GET /invoices?user_id=99999 → 타인 청구서?
POST 본문 (API):
// 정상: 본인 계좌에서 이체
{
"from_account": "ACC-1001",
"to_account": "ACC-2002",
"amount": 10000
}
// 공격: from_account를 타인 계좌로 변조
{
"from_account": "ACC-9999",
"to_account": "ACC-2002",
"amount": 10000
}
숨겨진 파라미터: 응답 JSON에서 노출된 필드를 요청 파라미터로 재사용하는 경우도 있습니다.
// 응답에 document_id가 포함된 경우
{
"id": 42,
"document_id": 7789, // 이 필드가 다른 API에서 직접 사용된다면?
"title": "My Document"
}
// 공격자가 document_id를 변경해 다른 문서 접근 시도
GET /api/documents/7790/download
취약 코드와 안전 코드 비교
# Django REST Framework (취약)
class OrderDetailView(APIView):
def get(self, request, order_id):
# ❌ order_id만으로 조회 — 소유자 확인 없음
order = Order.objects.get(id=order_id)
return Response(OrderSerializer(order).data)
# Django REST Framework (안전)
class OrderDetailView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, order_id):
# ✅ 현재 인증 사용자 + order_id로 복합 조회
order = get_object_or_404(Order,
id=order_id,
customer=request.user)
return Response(OrderSerializer(order).data)
// Node.js Prisma (취약 vs 안전)
// ❌ 취약
async function getDocument(req, res) {
const doc = await prisma.document.findUnique({
where: { id: req.params.id }
});
res.json(doc);
}
// ✅ 안전 — userId로 범위 제한
async function getDocument(req, res) {
const doc = await prisma.document.findFirst({
where: {
id: req.params.id,
userId: req.user.id // 소유자 검증
}
});
if (!doc) return res.status(404).json({ error: 'Not found' });
res.json(doc);
}
사용자 범위 쿼리 패턴
모든 개별 엔드포인트에서 소유자 검증을 추가하는 대신, 쿼리 레벨에서 현재 사용자 범위를 자동으로 적용하는 패턴이 더 안전합니다.
# Django ORM — 사용자 범위 기본 쿼리셋
class DocumentViewSet(viewsets.ModelViewSet):
serializer_class = DocumentSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
# 모든 조회/수정/삭제가 이 쿼리셋 기준으로 동작
return Document.objects.filter(owner=self.request.user)
def perform_create(self, serializer):
# 생성 시 owner 자동 설정
serializer.save(owner=self.request.user)
// Spring Data JPA — 메서드 이름으로 소유자 검증
public interface OrderRepository extends JpaRepository<Order, Long> {
// orderId와 userId 모두 일치하는 경우만 반환
Optional<Order> findByIdAndUserId(Long id, Long userId);
// 모든 주문 조회도 userId 범위로 제한
List<Order> findAllByUserId(Long userId);
}
// 서비스 레이어
public Order getOrder(Long orderId, Long currentUserId) {
return orderRepository.findByIdAndUserId(orderId, currentUserId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
}
간접 참조 매핑
외부에 DB ID를 직접 노출하는 대신 세션 기반 매핑을 사용하는 방법도 있습니다:
# 세션에 사용자별 매핑 저장
def get_document(request, index):
# URL에는 1, 2, 3 같은 세션 내 인덱스만 노출
document_ids = request.session.get('accessible_documents', [])
try:
real_id = document_ids[int(index)]
except (IndexError, ValueError):
return HttpResponse(status=404)
document = Document.objects.get(id=real_id)
return JsonResponse(document.to_dict())
실용적으로는 UUID를 사용하면서 소유자 검증을 병행하는 것이 균형 잡힌 방법입니다.
탐지 자동화
# pytest로 IDOR 자동 테스트
import pytest
from django.test import Client
@pytest.mark.django_db
def test_idor_prevention():
alice = User.objects.create_user('alice', password='pass')
bob = User.objects.create_user('bob', password='pass')
# Alice의 문서 생성
doc = Document.objects.create(title='Secret', owner=alice)
# Bob으로 Alice의 문서 접근 시도
client = Client()
client.login(username='bob', password='pass')
response = client.get(f'/api/documents/{doc.id}/')
# 404 또는 403이어야 함
assert response.status_code in [403, 404]
# 응답 본문에 문서 내용이 없어야 함
assert 'Secret' not in response.content.decode()
버그바운티에서 IDOR
IDOR는 버그바운티 프로그램에서 가장 흔히 제출되는 취약점 중 하나입니다. 다음 체크리스트로 시스템을 점검합니다:
- 계정 두 개를 만들어 각자의 리소스 ID를 교차해서 접근해본다
- Burp Suite로 트래픽을 캡처해 ID가 노출된 모든 파라미터를 식별한다
- API 응답에 포함된 다른 사용자의 ID를 다른 엔드포인트에서 재사용해본다
- 삭제/수정 작업도 소유자 검증이 적용되는지 확인한다
소유자 검증은 모든 리소스 접근 작업(GET, POST, PUT, PATCH, DELETE)에 빠짐없이 적용되어야 합니다.
지난 글: Broken Access Control: 접근 제어 취약점 — OWASP Top 1위
다음 글: Path Traversal: 경로 순회로 서버 파일 탈취하기
읽어주셔서 감사합니다. 😊