메뉴
HN
Hacker News 5일 전

슬래시 하나로 AWS API 인증 우회, 1.2만 달러 버그바운티 획득

IMP
8/10
핵심 요약

보안 연구원이 핀테크 기업의 모바일 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) 재현' 등의 다른 관련 포스트 링크가 함께 포함되어 있습니다.)

원문 보기
원문 보기 (영어)
Friday, 10 April 2026 I bypassed AWS API Gateway auth with a trailing slash. Got $12K bounty. I was poking at a fintech's mobile API and noticed something that made no sense. GET /v1/accounts returned 401. GET /v1/accounts/ returned 200 with full account data. One character. Completely different security posture. What I was looking at The API ran on AWS HTTP API — the newer, cheaper alternative to REST API. Lambda authorizer checked a JWT against Cognito, returned an IAM policy. Standard. Routes in OpenAPI: YAML /v1/accounts : get : x-amazon-apigateway-integration : uri : arn:aws:apigateway:... /v1/accounts/{accountId} : get : x-amazon-apigateway-integration : uri : arn:aws:apigateway:... The authorizer ran on every request. But HTTP API makes two decisions: does this route exist, and does the authorizer allow it? Those two layers didn't agree on what a "match" meant. The weird results I ran ffuf on the path. The results were… inconsistent. Request Response GET /v1/accounts 401 Unauthorized GET /v1/accounts/ 200 OK + full data GET /v1/accounts// 200 OK GET /v1/accounts?foo=bar 401 Unauthorized GET /v1/accounts%2f 404 Not Found The pattern: any path that sort-of matched a route prefix triggered the authorizer, then fell through to the integration without re-checking auth. HTTP API does greedy path matching by default. /v1/accounts/ matched /v1/accounts as a prefix. The authorizer ran and returned Allow . Then the integration executed — but the integration mapping was fuzzy. The path got rewritten, the auth context got dropped, and suddenly I was inside without a valid JWT. How the bypass actually worked I traced it carefully. The $default route in HTTP API is a catch-all. The fintech had set it to return 404. But they'd also attached a mock integration for health checks at some point. That mock didn't check auth — just returned {"status": "ok"} . But /v1/accounts/ wasn't hitting the mock. It was hitting the real backend. API Gateway's greedy match rewrote the trailing-slash path, stripped the slash, and forwarded to the /v1/accounts integration. The auth check happened on the original path. The integration ran on the rewritten path. The rewrite dropped the auth context. I confirmed it with a custom header. The authorizer sets context.authorizer.userId . The integration reads it. When I hit /v1/accounts/ , the integration received userId: undefined . The integration didn't validate userId . It just returned all accounts for the API key — which wasn't even required here because auth was supposed to be the JWT. The real damage Same bypass worked on POST /v1/transfers/ . I could initiate wire transfers without a valid JWT. The backend checked that fromAccount belonged to the user. But userId was undefined , so it defaulted to a system account. I stopped after one $0.01 test transfer. It went through. Telling them I wrote it up. Screenshots of the 401 vs 200. The ffuf output. The exact path rewrite behavior. They fixed it the next day. Switched from HTTP API to REST API (stricter path matching) Added userId validation in every Lambda, not just the authorizer. I got $12,000 bounty for it. Planning to go to Dubai :) at April 10, 2026 Email This BlogThis! Share to X Share to Facebook Share to Pinterest 2 comments: Anonymous 26 May 2026 at 04:05 to bang russian chicks? Good Reply Delete Replies Reply Anonymous 26 May 2026 at 04:07 Abas Reply Delete Replies Reply Add comment Load more... Newer Post Home Subscribe to: Post Comments (Atom) I built a scanner that found 41 live AWS keys in 900 Terraform state files I built a scanner that guesses S3 bucket names and looks for .tfstate files. Terraform state is a JSON file that happens to contain all you... I bypassed AWS API Gateway auth with a trailing slash. Got $12K bounty. I was poking at a fintech's mobile API and noticed something that made no sense. GET /v1/accounts returned 401. GET /v1/accounts/ returned... I reproduced a Claude Code RCE. The bug is everywhere.   Last week, security researcher Joernchen published a clever RCE in Claude Code 2.1.118 . I spent Saturday reproducing it from the advis... I poisoned a Hugging Face dataset and it stayed up for 6 months I uploaded a "fine-tuning dataset" to Hugging Face with 1,000 rows of clean code and 50 rows of backdoored examples. The backdoor: any funct...