당신의 서비스에는 클라우드 스토리지에 접근할 수 있는 API 키가 있다. 사용자가 요청을 본다. 서비스는 충실하게 파일을 작성한다. 그 파일은 당신의 버킷이 아닌 공격자의 버킷에 저장되고, 이제 공격자는 당신의 데이터를 손에 넣는다. 당신은 방금 눈치 채지 못한 공범이 되었다.
이것이 바로 confused deputy 공격이다. 공격자는 대상 버킷에 직접 쓸 수 없다. 그들에게는 해당 권한이 없다. 하지만 권한을 가진 당신의 서비스를 속여 대신 하도록 만들 수 있다. 당신의 서비스가 바로 “deputy”(대리인)이다. 이 대리인은 그 행동이 실제로 누구를 위한 것인지 “confused”(혼란스러워)하고 있다.
Confused Deputy 공격이 실제로 어떻게 보이는가
가장 고전적인 예는 컴파일러다. 컴파일러는 사용자의 권한으로 실행되므로, 사용자의 소스 파일을 읽고 빌드 디렉터리에 오브젝트 파일을 쓸 수 있다. 악의적인 사용자는 컴파일러에 출력을 빌드 디렉터리가 아닌 /etc/passwd에 쓰도록 지시하는 소스 파일을 입력한다. 컴파일러는 권한을 가지고 있다. 사용자는 없다. 컴파일러는 누구의 의도를 실행하고 있는지 혼란스러워 한다.
현대의 웹 서비스는 이 패턴을 끊임없이 재현한다. 사용자가 콜백 URL을 제공할 수 있는 파일 업로드 서비스를 생각해 보자:
# The user provides a URL. We fetch it and store it.
def upload_from_url(user_id: str, source_url: str, dest_path: str):
response = requests.get(source_url, timeout=30)
s3.put_object(Bucket="my-app-bucket", Key=dest_path, Body=response.content)
audit_log.info(f"User {user_id} uploaded to {dest_path}")
이 코드는 순진해 보인다. 사용자가 URL을 제공한다. 우리는 가져오고, 버킷에 저장한다. 하지만 만약 source_url이 http://169.254.169.254/latest/meta-data/iam/security-credentials/라면? 이제 우리는 자신의 AWS 자격 증명을 유출하여 공격자가 지정한 경로에 기록하고 있다. 우리의 IAM 역할은 권한을 가지고 있다. 공격자는 없다. 우리가 confused deputy인 것이다.
이 공격은 특권을 가진 구성 요소가 덜 특권을 가진 요청자를 대신해 행동하고, 요청자가 그 행동의 매개변수에 영향을 줄 수 있는 모든 시스템으로 일반화된다.
왜 신원 확인만으로는 충분하지 않은가
본능적인 수정책은 요청자가 누구인지 확인하는 것이다. “이 사용자가 인증되었는가?” 예. “이 사용자가 파일 업로드를 할 권한이 있는가?” 예. 두 가지 확인을 모두 통과했지만 공격은 여전히 통과한다.
문제는 권한 부여 확인이 요청자가 행동을 수행할 수 있는지를 검증할 뿐, 그 행동의 대상이 해당 요청자에게 적절한지는 검증하지 않는다는 점이다. 사용자는 업로드가 허용된다. 하지만 우리의 서비스가 자신의 메타데이터를 가져와 공격자의 통제 하에 저장하도록 만드는 것은 허용되지 않는다. 권한 모델은 “업로드 엔드포인트를 사용할 수 있다”는 것과 “서비스가 임의의 특권 행동을 수행하도록 만들 수 있다”는 것을 구분하지 못하고 있다.
이것이 핵심 실패 모드다: deputy는 능력(AWS 자격 증명, 파일 시스템 접근, 데이터베이스 쓰기 접근)을 보유하고 다른 누군가의 지시에 따라 이를 사용한다. 능력을 원래 권한 보유자의 구체적인 의도에 바인딩하지 않으면 혼란은 불가피하다.
능력 기반 보안: 권한을 의도에 바인딩하기
능력 기반 보안은 권한을 위조 불가능하고 명시적으로 만들어 이 문제를 해결한다. “당신은 누구인가?”라고 묻고 허용 목록을 조회하는 대신, 능력은 특정 리소스에 대한 특정 행동을 수행할 권리를 직접 전달하는 토큰이다.
능력 시스템에서 업로드 서비스는 범용적인 S3 쓰기 키를 보유하지 않는다. 특정 경로에, 특정 사용자에게 범위가 지정된 쓰기 능력을 보유한다. 사용자의 요청에는 정확한 대상을 지정하는 능력 토큰이 포함된다. 서비스는 사용자의 신원을 전역 권한 테이블과 대조하는 것이 아니라, 토큰의 암호화 서명과 범위를 검증한다.
범위가 지정된 토큰을 사용한 단순화된 예를 보자:
import hashlib
import hmac
import secrets
SECRET = secrets.token_bytes(32)
def mint_capability(principal: str, action: str, resource: str) -> str:
"""Create an unforgeable capability token scoped to a specific action and resource."""
payload = f"{principal}:{action}:{resource}"
sig = hmac.new(SECRET, payload.encode(), hashlib.sha256).hexdigest()[:16]
return f"{payload}:{sig}"
def verify_capability(token: str, expected_action: str, expected_resource: str) -> bool:
"""Verify a capability token matches the expected action and resource."""
try:
principal, action, resource, sig = token.rsplit(":", 3)
except ValueError:
return False
payload = f"{principal}:{action}:{resource}"
expected_sig = hmac.new(SECRET, payload.encode(), hashlib.sha256).hexdigest()[:16]
return hmac.compare_digest(sig, expected_sig) and action == expected_action and resource == expected_resource
# The user receives a capability that only allows writing to their own prefix.
user_id = "alice"
scoped_token = mint_capability(user_id, "write", f"uploads/{user_id}/report.pdf")
# Later, the service verifies the capability before acting.
if not verify_capability(scoped_token, "write", "uploads/alice/report.pdf"):
raise PermissionError("Capability does not match requested action or resource")
s3.put_object(Bucket="my-app-bucket", Key="uploads/alice/report.pdf", Body=data)
핵심 통찰은 다음과 같다: 능력 토큰 scoped_token은 uploads/bob/salary.xlsx에 쓰기 위해 재사용될 수 없다. 이 토큰은 생성 시점에 특정 리소스에 바인딩된다. 서비스는 누가 요청하는지에 따라 허용 여부를 결정하지 않는다. 토큰이 명시적으로 권한 부여하는 내용에 따라 결정한다.
능력 시스템이 실용적이지 않을 때
우리 대부분은 순수한 능력 아키텍처를 가진 운영 체제를 구축하고 있지 않다. 우리는 AWS API를 호출하고, PostgreSQL과 통신하고, HTTP 핸들러를 작성한다. 완전한 능력 기반 보안은 설계 철학이지, 드롭인 라이브러리가 아니다.
실용적인 중간 지점은 서비스 경계에 능력 사고방식을 적용하는 것이다. 서비스에 광범위한 S3 권한을 가진 단일 IAM 역할을 부여하는 대신, 범위가 지정된 presigned URL을 사용하라. 사용자가 임의의 대상을 이름 짓게 하는 대신, 해당 사용자가 접근할 수 있는 대상 리소스에 대해 검증하고 정제하라. 목표는 형식적인 능력 OS가 아니다. 목표는 암묵적 권한을 제거하는 것이다.
다음은 범용 신뢰 대신 범위 검증을 적용한 동일한 업로드 엔드포인트다:
import re
# A regex that constrains what a user can name as a destination.
ALLOWED_DEST_PATTERN = re.compile(r"^uploads/(?P<user_id>[a-z0-9_-]+)/[a-zA-Z0-9_.-]+$")
def upload_from_url(user_id: str, source_url: str, dest_path: str):
match = ALLOWED_DEST_PATTERN.match(dest_path)
if not match or match.group("user_id") != user_id:
raise ValueError("Invalid destination path")
# The source URL is also restricted. No internal IPs, no metadata endpoints.
parsed = urlparse(source_url)
if parsed.hostname in ("169.254.169.254", "localhost", "127.0.0.1"):
raise ValueError("Forbidden source URL")
response = requests.get(source_url, timeout=30)
s3.put_object(Bucket="my-app-bucket", Key=dest_path, Body=response.content)
이것은 형식적인 의미에서의 능력 시스템은 아니다. 하지만 동일한 원칙을 적용한다: 서비스의 권한은 공백 수표가 아니다. 요청자가 호출할 수 있는 특정 행동 집합에 범위가 지정된다.
왜 이 패턴이 끊임없이 재현되는가
Confused Deputy 취약점은 OAuth 흐름, 서버리스 함수, 웹훅 핸들러, 공급망 파이프라인 등에 나타난다. 특권을 가진 구성 요소가 사용자 입력을 처리하고 그에 따라 행동할 때마다 위험이 존재한다.
SSRF에서 메타데이터 유출로 이어지는 패턴은 너무 흔해서 클라우드 제공업체들이 이제 VPC에서 IMDS IP를 기본적으로 차단한다. 이것은 한 가지 증상에 대한 완화책이지, 질병에 대한 치료가 아니다. 질병은 자신의 의도와 요청자의 의도를 구분할 수 없는 특권을 가진 구성 요소다.
실무에서 Confused Deputy 공격 방지하기
아키텍처를 뜯어고칠 필요는 없다. 세 가지 습관이 대부분의 경우를 커버한다.
자격 증명의 범위를 좁혀라. 서비스가 S3와 통신한다면, 특정 접두사에만 쓸 수 있는 IAM 정책을 부여하라. 데이터베이스와 통신한다면 행 수준 보안을 사용하거나 테넌트별로 별도의 자격 증명을 사용하라. confused deputy의 영향 범위가 작을수록 그것이 일으킬 수 있는 피해도 적어진다.
행위자만 검증하지 말고 대상도 검증하라. 권한 부여 확인은 두 가지 질문에 답해야 한다: 요청자가 이 행동을 수행할 권한이 있는가, 그리고 행동의 대상이 요청자의 범위 내에 있는가? 사용자는 파일을 업로드할 수 있다. 다른 사용자의 디렉터리에 파일을 업로드할 수도 있는가? 서비스가 임의의 URL을 호출하도록 만들 수도 있는가? 두 번째 질문이 바로 confused deputy가 서식하는 곳이다.
주변 권한을 피하라. 주변 권한은 프로세스가 무엇을 하고 있는지가 아니라 누구인지에 따라 권한을 가지는 것이다. root로 실행하기, 마스터 API 키 사용하기, 데이터베이스 슈퍼유저 연결 유지하기 등이 있다. 이것들은 편리하고 위험하다. 특권을 분할하라. 명시적 만료가 있는 임시 자격 증명을 사용하라. 권한이 세분화될수록 혼란스럽게 만들기가 어려워진다.
FAQ
CSRF가 confused deputy 공격인가?
일정한 의미에서 그렇다. 사용자의 브라우저가 deputy다. 세션 쿠키(능력)를 보유하고 악의적인 사이트가 지시했기 때문에 상태를 변경하는 행동을 수행한다. 브라우저는 요청이 사용자의 의도를 나타내는지에 대해 혼란스러워 한다. CSRF 토큰은 능력(쿠키)을 특정 행동 맥락에 바인딩하여 작동한다. 이것이 바로 능력 기반 해결책이다.
이것이 마이크로서비스에도 적용되는가?
물론이다. 서비스 간 호출은 종종 광범위한 권한을 가진 공유 서비스 계정을 사용한다. 서비스 A가 사용자가 제공한 매개변수와 함께 서비스 B를 호출하고, 서비스 B가 자신의 특권 계정을 사용해 해당 매개변수에 따라 행동하면, 서비스 B가 confused deputy가 된다. 범위가 지정된 서비스 토큰이나 요청별 권한 부여를 사용하라.
SQL 인젝션은 어떤가?
SQL 인젝션은 데이터베이스에 대한 confused deputy 공격이다. 데이터베이스가 deputy다. 테이블을 읽고 쓸 권한을 가지고 있다. 공격자는 입력으로 위장한 지시를 데이터베이스에 전달한다. 매개변수화된 쿼리는 데이터베이스의 권한(쿼리 구조)을 사용자의 입력(매개변수)에서 분리하여 이 문제를 해결한다.
당신의 Deputy를 감사하라
서비스를 돌아보며 사용자가 가지지 않은 특권을 보유한 서비스를 식별하라. 각각에 대해 다음을 물어라: 사용자가 이 서비스를 속여 공격자에게 이익이 되는 방식으로 권한을 사용하도록 만들 수 있는가? 답이 예라면, confused deputy가 있는 것이다. 해결책은 드물게 이례적이지 않다. 서비스가 수행하려는 것의 범위를 좁히고, 그 범위를 벗어나는 요청을 거부하는 것이 보통이다.
당신의 서비스는 공범이 되어서는 안 된다. 누구의 명령을 따르고 있는지 확실히 하라.