“서버 푸시 구독 endpoint” 미검증 → SSRF 위험
들어가며
쉽게 말하면, 이 내용의 핵심은 딱 하나입니다.
“사용자가 입력한 주소로 우리 서버가 대신 접속해 주는데, 그 주소가 안전한 주소인지 확인하지 않고 그대로 믿고 있다.”
이게 바로 SSRF 위험입니다.
1. 현재 구조를 아주 쉽게 보면
현재 서버에는 이런 기능이 있다고 가정해보겠습니다.
사용자 브라우저
↓
/api/push/subscribe
↓
"내 알림을 여기로 보내주세요"
↓
endpoint 주소 저장
예를 들어 정상적인 사용자는 이런 주소를 보냅니다.
https://fcm.googleapis.com/...
서버는 이 주소를 저장해 둡니다.
그리고 나중에 축구 경기에서 골이 들어오면
골 발생
↓
PushNotificationDispatcher 실행
↓
저장해 둔 endpoint 확인
↓
WebPushSender가 HTTP POST
↓
https://fcm.googleapis.com/... 로 알림 전송
여기까지는 정상입니다.
2. 그런데 문제가 되는 부분
현재 /api/push/subscribe에서 endpoint를 받을 때
if (endpoint != null && !endpoint.isEmpty()) {
저장();
}
정도만 검사한다는 이야기입니다.
즉 서버 입장에서는
"https://fcm.googleapis.com/..."
도 저장하고,
"http://169.254.169.254/..."
도 저장할 수 있다는 것입니다.
서버 입장에서는 둘 다 그냥 “비어 있지 않은 문자열”이기 때문입니다.
누군가 브라우저 버튼을 아예 거치지 않고 curl이나 Postman 같은 걸로 직접POST /api/push/subscribe {"endpoint":"http://169.254.169.254/...", "keys":{...}, "deviceId":"..."}이렇게 요청을 보내면 서버는 그걸 진짜 브라우저 구독인 것처럼 그대로 믿고 저장한다. 즉 공격 경로는 화면의 구독 버튼을 통해서가 아니라, 그 버튼이 최종적으로 호출하는 서버 API 자체를 직접 두드리는 방식이다. 즉, “버튼을 거치지 않고 API에 직접 이상한 값을 찔러넣는 것” |
3. 169.254.169.254가 왜 문제인가?
여기가 핵심입니다.
169.254.169.254는 클라우드 환경에서 매우 중요한 내부 주소로 사용되는 경우가 많습니다.
예를 들어 AWS 같은 환경에서는 인스턴스 메타데이터 서비스와 연결되는 주소로 사용됩니다.
일반 사용자가 인터넷에서 여러분 서버의 내부 환경에 직접 접근할 수 없는 상황에서도,
인터넷 사용자
X
↓
내부 메타데이터 서버
직접 접근은 막혀 있을 수 있습니다.
그런데 여러분의 서버가 대신 요청해 준다면 이야기가 달라집니다.
공격자
↓
여러분 서버
↓
169.254.169.254
여러분 서버는 내부 네트워크에 있으니까 해당 주소에 접근할 수 있을 가능성이 있습니다.
이런 식으로 “서버가 공격자가 지정한 주소에 대신 요청하도록 만드는 것”이 SSRF(Server-Side Request Forgery)입니다.
4. 실제 공격 상황을 예로 들어보면
악의적인 사용자가 /api/push/subscribe에 이런 요청을 보냅니다.
{
"endpoint": "http://169.254.169.254/..."
}
현재 코드가 단순히
endpoint != null && !endpoint.isEmpty()
만 확인한다면?
서버는 이렇게 생각합니다.
endpoint 있음?
→ 있음
저장!
그러면 DB에 공격자가 원하는 주소가 들어갑니다.
push_subscription
────────────────────────────────
endpoint
http://169.254.169.254/...
그리고 얼마 후 축구 경기에서 골이 발생합니다.
손흥민 골!
↓
PushNotificationDispatcher
↓
DB에서 구독자 endpoint 조회
↓
WebPushSender
↓
http://169.254.169.254/...
결국 공격자가 직접 내부 서버에 접근한 것이 아니라,
“여러분의 서버를 프록시처럼 이용해서”
내부 주소에 요청을 보내게 만든 것입니다.
5. VAPID 서명까지 붙는다는 건 무슨 뜻인가?
여기서 WebPushSender가 단순 HTTP 요청만 보내는 게 아니라 Web Push용 VAPID 서명까지 붙인다는 내용이 있습니다.
정상적인 구조에서는
서버
↓
푸시 서비스 endpoint
↓
사용자 브라우저
를 위한 정상적인 인증 정보입니다.
그런데 endpoint 검증이 없다면 서버가 이런 정보를 붙여서 공격자가 지정한 주소에 요청하게 될 수 있다는 점이 문제입니다.
즉,
"우리 서버가 정상적인 Web Push 요청을 만들어서
공격자가 지정한 주소로 보내준다"
라는 상황이 만들어질 수 있습니다.
다만 여기서 중요한 것은 “VAPID 자체가 취약하다”는 뜻은 아닙니다.
문제는 endpoint 검증이 없다는 것입니다.
6. 그래서 어떻게 고치라는 이야기인가?
가장 간단한 방향은
“아무 URL이나 endpoint로 인정하지 말자.”
입니다.
예를 들어 최소한
https://
인지 확인하고,
허용할 수 있는 Push 서비스 도메인을 제한하는 방식입니다.
예를 들면 개념적으로:
if (!endpoint.startsWith("https://")) {
reject();
}
그리고 추가로:
fcm.googleapis.com
updates.push.services.mozilla.com
push.apple.com
...
처럼 허용된 Push 서비스만 받아들이는 것입니다.
즉,
https://fcm.googleapis.com/...
↓
허용
https://updates.push.services.mozilla.com/...
↓
허용
http://169.254.169.254/...
↓
차단
http://localhost/...
↓
차단
http://127.0.0.1/...
↓
차단
이런 구조를 만들자는 의미입니다.
7. 그런데 도메인만 검사하면 100% 안전할까?
여기서 한 단계 더 중요한 부분이 있습니다.
단순히 문자열로
endpoint.startsWith("https://")
만 검사하는 것은 충분하지 않습니다.
예를 들어 URL 파싱을 제대로 하지 않고 문자열만 검사하면 우회 방법이 생길 수 있습니다.
그래서 보통은 다음과 같은 방어를 함께 생각합니다.
① URL 형식 검사
② HTTPS만 허용
③ 허용된 hostname만 허용
④ localhost / 127.0.0.1 / 사설 IP 차단
⑤ 169.254.169.254 같은 링크-로컬 주소 차단
⑥ DNS를 사용하는 경우 DNS 재해석 문제도 고려
⑦ HTTP redirect를 따라가지 않도록 설정
특히 마지막 부분이 중요합니다.
처음에는
https://fcm.googleapis.com/...
처럼 정상적인 주소였는데 서버가 요청했더니,
→ 302 Redirect
→ http://169.254.169.254/...
로 이동하도록 만들어 버리면 문제가 생길 수 있습니다.
따라서 WebPushSender 쪽의 HTTP 클라이언트가 redirect를 자동으로 따라가는지도 확인해야 합니다.
8. 이 문제를 한 문장으로 정리하면
현재 구조는:
사용자
↓
"알림 받을 주소는 여기예요"
↓
서버가 주소 검증
↓
❌ 비어 있나?
↓
DB 저장
↓
골 발생
↓
서버가 그 주소로 POST
인데,
안전하게 만들려면:
사용자
↓
"알림 받을 주소는 여기예요"
↓
서버
↓
URL 검증
↓
HTTPS인가?
↓
허용된 Push 서비스인가?
↓
내부 IP인가?
↓
localhost인가?
↓
정상 endpoint인가?
↓
✅ DB 저장
↓
골 발생
↓
푸시 전송
으로 바꾸자는 것입니다.
9. 개발자 관점에서 가장 중요한 포인트
이 취약점의 이름을 기억하기보다는 다음 상황을 기억하면 이해하기 쉽습니다.
외부 사용자가 URL을 입력한다
+
서버가 그 URL로 HTTP 요청한다
↓
SSRF 위험
현재 말씀하신 코드에서는 정확히 이 구조가 만들어져 있습니다.
특히
PushSubscriptionServlet
↓
endpoint를 외부에서 받음
↓
DB에 저장
↓
PushNotificationDispatcher
↓
WebPushSender
↓
저장된 endpoint로 서버가 직접 POST
이 흐름을 확인하는 것이 중요합니다.
즉, 이 지적은 단순히 “URL 검사를 좀 더 꼼꼼하게 하세요” 정도가 아니라,
“외부 사용자가 지정한 URL을 서버가 나중에 직접 호출하는 구조 자체가 있으므로, 그 URL을 신뢰하면 안 된다.”
라는 보안 지적입니다.
그리고 현재 개발 중인 코드라면 가장 먼저 PushSubscriptionServlet.java의 endpoint 저장 부분과 WebPushSender.java의 HTTP 요청 부분을 같이 확인해 보는 게 좋습니다.
홍TV



![Eclipse에서 갑자기 "[m2e] Lifecycle Mapping" 오류? Maven 프로젝트가 빨간 줄 뜨는 이유 4 Eclipse에서 갑자기 [m2e] Lifecycle Mapping 오류? Maven 프로젝트가 빨간 줄 뜨는 진짜 이유](https://hongtv.co.kr/wp-content/uploads/2026/09/62edeb0f-aaf8-42cc-af31-1a91e54bc187-300x200.png)
댓글 0
첫 댓글을 남겨보세요.