Broken Access Control: 접근 제어 취약점 — OWASP Top 1위
OWASP Top 10 2021 1위인 접근 제어 취약점의 6가지 유형(IDOR, 권한 상승, 경로 순회 등), 취약한 코드 패턴, RBAC 미들웨어와 소유자 검증으로 방어하는 방법을 다룹니다.
지난 글에서 SSRF를 통한 클라우드 메타데이터 탈취를 살펴봤습니다. 이번 글에서는 **OWASP Top 10 2021에서 1위를 차지한 Broken Access Control(접근 제어 취약점)**을 다룹니다. OWASP 데이터에 따르면 테스트된 애플리케이션의 94%에서 이 취약점이 발견될 만큼 만연합니다.
접근 제어란?
접근 제어는 인증된 사용자가 자신이 허용된 범위 내에서만 행동할 수 있도록 제한하는 메커니즘입니다. “누구인지”를 확인하는 인증(Authentication)과 달리, “무엇을 할 수 있는지”를 제어하는 인가(Authorization)가 접근 제어의 핵심입니다.
Broken Access Control은 이 인가 로직이 누락되거나 잘못 구현된 경우를 총칭합니다.
6가지 주요 취약점 유형
① IDOR (Insecure Direct Object Reference): 사용자 입력으로 받은 식별자(ID)를 소유자 검증 없이 직접 DB 조회에 사용합니다. 가장 흔히 발견되는 패턴입니다.
② 수평/수직 권한 상승: 수평 권한 상승은 같은 권한 레벨의 다른 사용자 리소스에 접근하는 것, 수직 권한 상승은 더 높은 권한(관리자)으로 격상하는 것입니다.
③ 경로 순회: ../를 이용해 허용된 디렉토리 밖의 파일에 접근합니다.
④ 기능 수준 접근 제어 누락: UI에서 관리자 메뉴를 숨겼지만 API 엔드포인트에는 권한 검사가 없습니다.
⑤ 질량 할당: ORM 프레임워크에서 사용자가 전송한 모든 필드를 모델에 바인딩할 때 role, isAdmin 같은 민감 필드도 함께 수정됩니다.
⑥ 메타데이터 조작: JWT 서명을 검증하지 않거나 쿠키 값을 서버에서 재검증하지 않아 권한을 위조할 수 있습니다.
IDOR 취약점 상세
# Django (취약)
@login_required
def get_invoice(request, invoice_id):
# ❌ invoice_id가 현재 사용자 것인지 확인 안 함
invoice = Invoice.objects.get(id=invoice_id)
return JsonResponse(invoice.to_dict())
# Django (안전)
@login_required
def get_invoice(request, invoice_id):
# ✅ 현재 사용자의 인보이스만 조회
try:
invoice = Invoice.objects.get(id=invoice_id, user=request.user)
except Invoice.DoesNotExist:
return HttpResponse(status=404) # 403 대신 404로 존재 자체를 숨김
return JsonResponse(invoice.to_dict())
예측 가능한 순차 ID(1, 2, 3…) 대신 UUID를 사용하면 IDOR 탐지를 어렵게 만들 수 있지만, 소유자 검증을 대체하지는 않습니다:
import uuid
# 순차 ID 대신 UUID 사용
class Invoice(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(User, on_delete=models.CASCADE)
# ...
접근 제어 구현 패턴
RBAC 미들웨어: 역할 기반 접근 제어를 재사용 가능한 미들웨어로 구현합니다.
# Python Flask RBAC 데코레이터
from functools import wraps
from flask import g, jsonify
def require_role(*roles):
def decorator(f):
@wraps(f)
def decorated(*args, **kwargs):
if not g.current_user:
return jsonify({'error': 'Unauthorized'}), 401
if g.current_user.role not in roles:
return jsonify({'error': 'Forbidden'}), 403
return f(*args, **kwargs)
return decorated
return decorator
@app.route('/admin/users')
@require_role('admin', 'superadmin')
def list_users():
return jsonify(User.query.all())
질량 할당 방어:
# Django — 허용 필드 명시
class UserUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = User
# ✅ 수정 가능한 필드만 명시
fields = ['name', 'email', 'bio']
# role, is_staff, is_superuser는 제외
# Rails — Strong Parameters
def user_params
params.require(:user).permit(:name, :email, :bio)
# role은 포함하지 않음
end
경로 순회 방어:
import os
def safe_file_access(filename: str, base_dir: str) -> str:
# 절대 경로로 정규화
real_base = os.path.realpath(base_dir)
requested = os.path.realpath(os.path.join(base_dir, filename))
# base_dir 밖을 벗어나는지 검사
if not requested.startswith(real_base + os.sep):
raise ValueError('경로 순회 시도 감지')
return requested
접근 제어 설계 원칙
기본 거부(Deny by Default): 명시적으로 허용되지 않은 모든 접근은 거부합니다.
# 명시적 허용 목록 (화이트리스트)
ALLOWED_ACTIONS = {
'admin': ['read', 'write', 'delete', 'manage_users'],
'editor': ['read', 'write'],
'viewer': ['read'],
}
def can(user, action):
return action in ALLOWED_ACTIONS.get(user.role, [])
모든 계층에서 검증: 프론트엔드(UI 숨김)만이 아니라 API, 서비스 레이어, DB 쿼리까지 모든 계층에서 권한을 검증합니다.
감사 로깅: 민감한 리소스 접근, 권한 거부 이벤트를 모두 로깅합니다.
import logging
security_logger = logging.getLogger('security')
def get_order(request, order_id):
order = Order.objects.filter(id=order_id, user=request.user).first()
if not order:
# 403 대신 404 — 자원 존재 여부 노출 방지
security_logger.warning(
f'Access denied: user={request.user.id} tried order={order_id}'
)
return HttpResponse(status=404)
return JsonResponse(order.to_dict())
자동화 테스트
접근 제어 취약점은 수동 테스트만으로 찾기 어렵습니다. 테스트 케이스를 코드로 작성해 CI/CD 파이프라인에 포함합니다:
# pytest — IDOR 테스트
def test_idor_prevention(client):
user_a = create_user('alice')
user_b = create_user('bob')
order = create_order(user=user_a)
# Bob이 Alice의 주문 접근 시도
client.force_login(user_b)
response = client.get(f'/api/orders/{order.id}/')
assert response.status_code == 404 # 403도 가능하나 404가 더 안전
접근 제어는 “한 번 구현하고 끝”이 아닙니다. 새 기능을 추가할 때마다 권한 검사를 빠뜨리지 않았는지 코드 리뷰와 테스트로 지속적으로 검증해야 합니다.
지난 글: SSRF와 클라우드 메타데이터: AWS/GCP/Azure 자격증명 탈취
다음 글: IDOR: 불안전한 직접 객체 참조 완전 분석
읽어주셔서 감사합니다. 😊