상위: Web Security
요약
IDOR(Insecure Direct Object Reference, 안전하지 않은 직접 객체 참조)는 사용자가 권한 검증 없이 다른 사용자의 리소스에 접근할 수 있는 취약점입니다. URL이나 파라미터의 ID 값을 변경하여 타인의 데이터를 조회/수정/삭제할 수 있습니다.
공격 원리
정상 요청:
GET /api/users/123/profile (내 프로필)
IDOR 공격:
GET /api/users/124/profile (다른 사용자 프로필)
GET /api/users/125/profile
GET /api/users/1/profile (관리자?)
핵심 문제: 서버가 "요청한 리소스에 접근 권한이 있는가?"를 확인하지 않음
공격 유형
1. 수평적 권한 상승 (Horizontal)
같은 권한 레벨의 다른 사용자 데이터 접근
사용자 A (user_id=100)
→ GET /orders/500 (A의 주문)
→ GET /orders/501 (B의 주문) ← IDOR!
결과: 다른 사용자의 주문 정보, 결제 내역, 개인정보 노출
2. 수직적 권한 상승 (Vertical)
더 높은 권한의 기능/데이터 접근
일반 사용자
→ GET /admin/users ← 관리자 페이지 접근
→ POST /api/users/123/role {"role": "admin"} ← 권한 상승
결과: 관리자 기능 사용, 권한 변경
3. 파일 기반 IDOR
다운로드:
GET /download?file=report_user123.pdf
GET /download?file=report_user124.pdf ← IDOR!
업로드된 파일:
GET /uploads/invoice_12345.pdf
GET /uploads/invoice_12346.pdf ← IDOR!
실제 공격 시나리오
시나리오 1: 전자상거래 주문 조회
취약한 API:
GET /api/orders/10001
공격:
# 주문 번호 순회
for i in {10001..10100}; do
curl "https://shop.com/api/orders/$i" -H "Cookie: session=xxx"
done
결과:
- 타인의 주문 내역
- 배송 주소, 연락처
- 결제 정보
시나리오 2: 은행 계좌 조회
GET /api/accounts/1234567890/balance
공격:
GET /api/accounts/1234567891/balance
GET /api/accounts/0000000001/balance (특별 계좌?)
시나리오 3: 의료 기록
GET /patient/records?patient_id=P12345
공격:
GET /patient/records?patient_id=P12346
→ 타인의 의료 기록 노출 (HIPAA 위반)
시나리오 4: 문서/파일 공유
정상: https://docs.company.com/view/abc123xyz
공격: https://docs.company.com/view/abc123xyz (다른 문서 ID 추측)
취약한 코드 vs 안전한 코드
취약한 코드
@app.route('/api/orders/<order_id>')
def get_order(order_id):
# 권한 검증 없음!
order = Order.query.get(order_id)
return jsonify(order.to_dict())
안전한 코드
@app.route('/api/orders/<order_id>')
@login_required
def get_order(order_id):
order = Order.query.get(order_id)
# 권한 검증
if order.user_id != current_user.id:
abort(403, "접근 권한이 없습니다")
return jsonify(order.to_dict())
더 안전한 코드 (쿼리 레벨 필터링)
@app.route('/api/orders/<order_id>')
@login_required
def get_order(order_id):
# 애초에 본인 주문만 조회 가능
order = Order.query.filter_by(
id=order_id,
user_id=current_user.id # 사용자 필터 포함
).first_or_404()
return jsonify(order.to_dict())
방어 방법
1. 접근 제어 검증 (필수)
def check_access(user, resource):
# 소유자 확인
if resource.owner_id == user.id:
return True
# 관리자 확인
if user.is_admin:
return True
# 공유된 리소스 확인
if user.id in resource.shared_with:
return True
return False
2. 간접 참조 사용
# 직접 참조 (취약)
GET /api/users/12345
# 간접 참조 (안전)
GET /api/users/me # 세션에서 사용자 식별
3. UUID 사용 (추측 어렵게)
import uuid
# 순차적 ID (취약)
order_id = 12345 # 쉽게 추측 가능
# UUID (안전)
order_id = uuid.uuid4() # 550e8400-e29b-41d4-a716-446655440000
주의: UUID만으로는 불충분! 여전히 권한 검증 필요
4. 서명된 URL (파일 접근)
import hmac
import time
def generate_signed_url(file_id, user_id, expires_in=3600):
expiry = int(time.time()) + expires_in
signature = hmac.new(
SECRET_KEY,
f"{file_id}:{user_id}:{expiry}".encode(),
'sha256'
).hexdigest()
return f"/download/{file_id}?user={user_id}&expires={expiry}&sig={signature}"
5. 레코드 레벨 보안
# Django
class OrderQuerySet(models.QuerySet):
def for_user(self, user):
if user.is_admin:
return self
return self.filter(user=user)
class Order(models.Model):
objects = OrderQuerySet.as_manager()
# 사용
orders = Order.objects.for_user(request.user).all()
테스트 방법
1. ID 값 변경
원본: /api/users/100
테스트: /api/users/101, /api/users/99, /api/users/1
2. 파라미터 조작
원본: /orders?user_id=100
테스트: /orders?user_id=101
테스트: /orders (user_id 제거)
3. HTTP 메서드 변경
GET /api/users/101 (읽기)
PUT /api/users/101 (수정)
DELETE /api/users/101 (삭제)
4. 자동화 도구
# Burp Suite Intruder
# 또는 간단한 스크립트
for id in $(seq 1 1000); do
response=$(curl -s "https://api.example.com/users/$id" -H "Authorization: Bearer $TOKEN")
if [ ! -z "$response" ]; then
echo "ID $id: $response"
fi
done
IDOR 발생하기 쉬운 기능
| 기능 | 파라미터 예시 |
|---|---|
| 사용자 프로필 | /users/{user_id} |
| 주문 조회 | /orders/{order_id} |
| 파일 다운로드 | /download?file_id=123 |
| 메시지/댓글 | /messages/{msg_id} |
| 결제 내역 | /payments/{payment_id} |
| API 키 관리 | /api-keys/{key_id} |
| 설정 페이지 | /settings?user=123 |
관련 공격
- Path Traversal - 파일 시스템 기반 접근 제어 우회
- SQL Injection - 데이터베이스 레벨 접근 제어 우회
- Session Hijacking - 다른 사용자 세션 탈취