슬래시 하나로 AWS API 인증 우회, 1.2만 달러 버그바운티 획득
보안 연구원이 핀테크 기업의 모바일 API를 테스트하던 중 URL 끝에 슬래시(/) 하나를 추가해 AWS API Gateway의 인증을 우회할 수 있는 취약점을 발견했습니다. AWS HTTP API의 탐욕적 경로 매칭(Greedy path matching) 과정에서 경로 재작성(Path rewrite) 시 인증 컨텍스트가 소실되는 설계적 모순이 원인으로, 이를 통해 계좌 정보 탈취 및 무단 이체까지 가능했습니다. 해당 기업은 다음 날 REST API로 전환하고 백엔드에 userId 검증 로직을 추가하여 문제를 신속히 해결했으며, 연구원은 1만 2천 달러의 포상금을 받았습니다.
2026년 4월 10일 금요일
슬래시(/) 하나로 AWS API Gateway 인증을 우회해 1만 2천 달러(약 1,600만 원)의 버그 바운티를 받았습니다.
저는 한 핀테크 기업의 모바일 API를 테스트하던 중 이해할 수 없는 기이한 현상을 발견했습니다.
GET /v1/accounts는 401 에러를 반환했습니다. GET /v1/accounts/는 200 OK와 함께 전체 계좌 데이터를 반환했습니다.
단 한 글자 차이였지만, 보안 태세(Security posture)는 완전히 달랐습니다.
제가 마주친 상황 해당 API는 AWS HTTP API(기존 REST API를 대체하는 더 새롭고 저렴한 옵션)에서 실행되고 있었습니다. Lambda 권한 부여자(Authorizer)가 Cognito를 통해 JWT(JSON Web Token)를 검증하고 IAM 정책을 반환하는 아주 표준적인 구조였습니다.
OpenAPI의 경로(Routes) 설정은 다음과 같았습니다:
YAML /v1/accounts : get : x-amazon-apigateway-integration : uri : arn:aws:apigateway:... /v1/accounts/{accountId} : get : x-amazon-apigateway-integration : uri : arn:aws:apigateway:...
모든 요청에 대해 권한 부여자가 실행되었습니다. 하지만 HTTP API는 두 가지 단계를 거칩니다. 첫째, 이 경로가 존재하는가? 둘째, 권한 부여자가 이를 허용하는가? 그런데 바로 이 두 레이어가 '일치(Match)'의 의미를 두고 서로 엇갈린 판단을 내리고 있었습니다.
기이한 결과들 저는 경로에 대해 ffuf(퍼징 도구)를 실행해 보았습니다. 결과는 참으로... 불일치했습니다.
요청 -> 응답 GET /v1/accounts -> 401 Unauthorized GET /v1/accounts/ -> 200 OK + 전체 데이터 GET /v1/accounts// -> 200 OK GET /v1/accounts?foo=bar -> 401 Unauthorized GET /v1/accounts%2f -> 404 Not Found
여기서 발견된 패턴은 다음과 같았습니다. 경로 접두사와 어느 정도 일치하는 모든 경로가 권한 부여자를 트리거한 뒤, 인증을 다시 확인하지 않은 채 통합(Integration) 단계로 그냥 통과시켜 버린다는 것입니다.
HTTP API는 기본적으로 탐욕적 경로 매칭(Greedy path matching)을 수행합니다. /v1/accounts/가 /v1/accounts라는 접두사와 일치하는 것으로 간주된 것입니다. 권한 부여자가 실행되어 '허용(Allow)'을 반환했습니다. 그런 다음 통합이 실행되었는데, 이때 통합 매핑이 모호하게 작동했습니다. 경로가 재작성되고 인증 컨텍스트는 드롭되었으며, 순식간에 저는 유효한 JWT 없이도 시스템 내부에 들어가 있었습니다.
우회가 실제로 작동한 원리 저는 이 과정을 꼼꼼하게 추적해 보았습니다. HTTP API의 $default 경로는 모든 예외 요청을 잡는 Catch-all 역할을 합니다. 그 핀테크 기업은 이 경로가 404를 반환하도록 설정해 두었습니다. 하지만 언젠가 상태 확인(Health check)을 위해 모의 통합(Mock integration)을 하나 연결해 두었었습니다. 이 모의 통합은 인증을 검사하지 않고 그저 {"status": "ok"}만 반환했습니다.
하지만 /v1/accounts/는 이 모의 통합을 호출한 것이 아니었습니다. 실제 백엔드를 호출하고 있었습니다. API Gateway의 탐욕적 매칭이 후행 슬래시가 있는 경로를 재작성하여 슬래시를 제거한 뒤, /v1/accounts 통합으로 요청을 전달한 것입니다.
결국 인증 확인은 '원래 경로'에서 이루어졌고, 통합 실행은 '재작성된 경로'에서 이루어졌습니다. 바로 이 재작성 과정에서 인증 컨텍스트가 소실된 것입니다.
저는 커스텀 헤더를 통해 이 사실을 확인했습니다. 권한 부여자는 context.authorizer.userId를 설정하고, 통합은 이를 읽어들입니다. 제가 /v1/accounts/를 호출했을 때, 통합은 userId: undefined를 받았습니다. 하지만 통합에서는 userId를 검증하지 않았습니다. 단지 API 키에 해당하는 모든 계좌를 반환했을 뿐이었습니다. 사실 이곳에서는 API 키조차 필요하지 않았는데, 원래 인증 방식이 JWT를 통해서만 이루어져야 했기 때문입니다.
실제 피해 규모 동일한 우회 방법이 POST /v1/transfers/에서도 그대로 먹혔습니다. 저는 유효한 JWT 없이도 계좌 이체를 시작할 수 있었습니다. 백엔드에서는 보내는 계좌(fromAccount)가 해당 사용자의 소유인지 확인했습니다. 그러나 userId가 undefined였기 때문에, 시스템 계정(System account)이 기본값으로 설정되었습니다.
저는 0.01달러의 테스트 이체를 한 번 시도한 뒤 멈췄습니다. 이체는 성공적으로 완료되었습니다.
기업에 제보하기 저는 이 내용을 보고서로 작성했습니다. 401 응답과 200 응답의 스크린샷, ffuf 출력 결과, 그리고 정확한 경로 재작성 동작 방식을 포함시켰습니다.
그 회사는 다음 날 바로 이 문제를 해결했습니다. HTTP API에서 REST API로 변경(더 엄격한 경로 매칭 적용) 권한 부여자뿐만 아니라 모든 Lambda 함수에 userId 검증 로직 추가
저는 이 사례에 대해 12,000달러의 버그 바운티를 받았습니다. 이 돈으로 두바이에 놀러 갈 계획입니다. :-)
2026년 4월 10일
(참고: 본 원문 블로그 게시물 하단에는 방문자들이 남긴 '러시아 여성을 만나러 가는 거냐' 등의 일련의 잡담성 댓글과 '41개의 라이브 AWS 키를 찾은 스캐너 제작기', '클로드 코드 원격 코드 실행(RCE) 재현' 등의 다른 관련 포스트 링크가 함께 포함되어 있습니다.)