SSRF : Server-Side Request Forgery
-> 서버 측 요청 위조
-> 공격자가 직접 요청하는 것이 아니라, 서버가 대신 요청하게 만드는 취약점
서버가 정확히 뭔지 , API에 대한 정리, 마이크로서비스에 대한 것을 먼저 정리 해봤음
서버에 대해..
- 요청을 받고 응답을 돌려주는 컴퓨터, 프로그램
보통 웹 서비스를 사용할 때 사용자는 브라우저에 요청을 보냄
사용자 브라우저 -> 서버 -> 응답
예를 들어서,
사용자가 로그인 페이지에 접속하면
브라우저는 서버에 요청을 보내고, 서버는 로그인 페이지 html을 응답해줌
GET / login
-> 이런 요청이 오면 서버가 로그인 페이지 달라는 요청이군 하고, 해당 기능을 실행해서 결과과를 응답으로 돌려줌
보통 웹개발 공부 같은 거 할 때 Flask를 본인 노트북이다 컴터로 실행하는데, 그럼 그때의 서버는 본인 노트북이나 컴이 되는 것.
http://127.0.0.1:5000 -> 로컬 호스트,, 내꺼,,
그니까 보통
개발 환경에서는,..
내 브라우저 -> 내 컴퓨터에서 실행 중인 Flask 서버
실제로는,,
사용자 브라우저 -> AWS, Naver Cloud, 회사 서버 등에서 실행 중인 웹서버
Flask
-> 웹 서버 개발에 필요한 복잡한 기본 동작들을 미리 만들어둔 도구, 개발자는 그걸 import해서 사용,,!
웹 서버가 하는
1. 브라우저에서 요청이 들어오는지 기다리기
2. 요청 내용 읽기
3. URL이 뭔지 확인하기
4. GET인지 POST인지 확인하기
5. /login이면 로그인 함수 실행하기
6. /posts면 게시글 함수 실행하기
7. HTML이나 JSON 응답 만들기
8. 브라우저에 응답 보내기
9. 쿠키, 세션, 헤더 같은 것도 처리하기
이런 일들을 전부 내가 코드로 직접 구현하지 않고
Flask를 써서 그냥 import해서 편하게 가져다 쓰는 것
API (Application Programming Interface)
-> 각 웹 서비스나 서버 기능을 사용하기 위해 "요청은 어떤 주소로, 어떤 방식으로, 어떤 데이터를 담아 보내야 하고, 서버는 어떤 형식으로 응답해야하는가?"와 같은 것들을 정해둔 규약
외부 API
- 다른 회사나 외부 서비스에서 제공하는 API
- 카카오 로그인 API, 네이버 지도 API, 토스 결제 API,,등등
내부 API
- 같은 서비브 내부에서 서버들끼리 사용하는 API
- 회원 서비스 API, 주문 서비스 API, 관리자 서비스 API,,등등
- 내부 API는 보통 외부 사용사자가 직접 접근하면 안됨
**
컴퓨터 안에서도 수많은 소프트웨어들이 API를 통해 상호작용하며 동작함
예를들어서,
- 브라우저에서도 개발자들이 사용할 수 있는 다양한 기능들을 API들을 통해 제공함
(DOM API, Timing API, Fetch API...)
- 윈도우 AP
이런 것들,,
**
SSRF
위에서 말했듯이, 말그대로 서버 측 요청 위조
-> 공격자가 직접 요청하는 것이 아니라 서버가 대신 요청을 보내게 만드는 취약점임
보통, 일반적 요청 흐름은
사용자 -> 웹 서버 -> 응답
근데, SSRF가 발생하면
공격자 -> 웹 서버 -> 내부 서버 , 외부 서버
즉 공격자가 직접 내부 서버에 접근하는 것이 아니라, 웹 서버에게 특정 URL로 요청을 보내게 만드는 것
결론적으로 정리하면
=> 공격자가 서버를 이용해서 원래 직접 접근할 수 없는 주소에 대신 요청을 보내게 만드는 취약점이다
웹 서비스가 보내는 요청을 변조하기 위해서는 요청 내에 이용자의 입력값이 포함돼야 하고
이렇게 입력값이 포함되는 예시로는
- 웹 서비스가 이용자가 입력한 URL에 요청을 보내거나
- 요청을 보낼 URL에 이용자 번호와 같은 내용이 사용되는 경우
- 이용자가 입력한 값이 HTTP Body에 포함되는 경우
이렇게 나눠 볼 수 있음
먼저,
1. 이용자가 입력한 URL에 요청을 보내는 경우의 예시
from flask import Flask, request
import requests
app = Flask(__name__)
@app.route("/image_downloader")
def image_downloader():
# 이용자가 입력한 URL에 HTTP 요청을 보내고 응답을 반환하는 페이지 입니다.
image_url = request.args.get("image_url", "")
response = requests.get(image_url)
return (
response.content,
200,
{"Content-Type": response.headers.get("Content-Type", "")},
)
@app.route("/request_info")
def request_info():
return request.user_agent.string
app.run(host="127.0.0.1", port=8000)
코드를 보면
image_downloader()함수에서 사용자가 입력한 image_url 값을 검증 없이 requests.get에 그대로 전달하고 있음.
그래서 실제로 환경을 빌드해서 간단하게 보면,
http://127.0.0.1:8000/image_downloader?image_url=https://dreamhack.io/assets/dreamhack_logo.png
이렇게 정상적인 값을 입력하면

정상적인 루트로 이미지를 불러온다.
근데, 여기서 ssrf취약점을 이용해서
원래는 그냥 브라우저 정보를 표시하는 /request_info 페이지 인데,

http://127.0.0.1:31339/image_downloader?image_url=http://127.0.0.1:31339/request_info
이렇게 입력을 하면, 웹서버가 스스로에게 요청을 보내서, 서버 코드가 request.get()으로 요청을 보낸것이 되면서
요청을 보낸 주체가 파이썬 request 라이브러리가 되는 것,,!!

이게, 만약에 단순하게 /request_info가 아니라
- /admin
- 내부 전용 API
- 외부에서 막아둔 관리자 페이지
- 내부 서비스 주쇠
- 클라우드 메타데이터 주소
이런것들이라면 위험할 수 있음..,.!
또한, 위 예시코드는 이것과 더불어
return (
response.content,
200,
{"Content-Type": response.headers.get("Content-Type", "")},
)
이렇게 응답 내용까지 그대로 반환하고 있기때문에 더 위험하다
-> 서버가 가져온 내용을 사용자가 그대로 볼 수 있기 때문!
두번째로
2. 웹 서비스의 요청 URL에 이용자의 입력값이 포함되는 경우
INTERNAL_API = "http://api.internal/"
# INTERNAL_API = "http://172.17.0.3/"
@app.route("/v1/api/user/information")
def user_info():
user_idx = request.args.get("user_idx", "")
response = requests.get(f"{INTERNAL_API}/user/{user_idx}")
@app.route("/v1/api/user/search")
def user_search():
user_name = request.args.get("user_name", "")
user_type = "public"
response = requests.get(f"{INTERNAL_API}/user/search?user_name={user_name}&user_type={user_type}")
user_info 같은 경우에는
이용자가 전달하는 user_idx값을 내부 API의 URL경로로 사용하고 있음을 볼 수 있는데
즉, user_idx=1로 설정해서
http://x.x.x.x/v1/api/user/information?user_idx=1 이렇게 되면
웹 서비스는 http://api.internal/user/1 이와 같은 주소에 요청을 보내게 됨
user_search 같은 경우에는
이용자가 전달하는 user_name 값을 내부 API의 쿼리로 사용함
정상적으로 보면, http://x.x.x.x/v1/api/user/search?user_name=hello 이런식으로 설정해서 요청을 보내면
http://api.internal/user/search?user_name=hello&user_type=public 이와 같은 주소에 요청을 보냄
위 2개 모두 이용자의 입력값이 포함되면서 요청 변조가 가능한데
만약 이용자의 입력값 중 URL의 구성요소 문자를 삽입한다면 API 경로 조작이 가능해짐
user_info 에서는,
user_idx=../serch 이렇게 입력하면
http://api.internal/user/1 이렇게 의도한 정상 경로가 아닌
http://api.internal/search 이렇게 경로를 변조 할 수 있음 --> Path Traversal
user_idx=../search?user_name=secret&user_type=private 이런식의 입력도 가능
user_serch 에서는,
user_name=secret&user_type=private# 이렇게 입력하면
http://api.internal/search?user_name=secret&user_type=private#&user_type=public
위와 같은 URL에 요청을 보내는데, 이때
# -> Fragment Identifier 구분자로 뒤에 붙은 문자열을 생략시키고, 그럼 위 URL은
실제로는
http://api.internal/search?user_name=secret&user_type=private
위와 같은 URL이 된다,,!
다만, 항상 이렇게 동작하는 건 아니고,
- requests 라이브러리가 URL을 어떻게 처리하는지
- 내부 웹 서버가 /../ 경로를 정규화하는지
- 프록시나 라우터가 경로를 정리하는지
등등의 요소에 따라 달라짐
그렇지만, 결국 사용자 입력이 내부 요청 URL의 path 구조를 바꿀 수 있다는 것 자체가 위험한 설계로 볼 수 있음!
3. 웹 서비스의 요청 Body에 이용자의 입력값이 포함되는 경우
from flask import Flask, request, session
import requests
from os import urandom
app = Flask(__name__)
app.secret_key = urandom(32)
INTERNAL_API = "http://127.0.0.1:8000/"
header = {"Content-Type": "application/x-www-form-urlencoded"}
@app.route("/v1/api/board/write", methods=["POST"])
def board_write():
session["idx"] = "guest" .
title = request.form.get("title", "")
body = request.form.get("body", "")
data = f"title={title}&body={body}&user={session['idx']}"
response = requests.post(f"{INTERNAL_API}/board/write", headers=header, data=data) # INTERNAL API 에 이용자가 입력한 값을 HTTP BODY 데이터로 사용해서 요청합니다.
return response.content
@app.route("/board/write", methods=["POST"])
def internal_board_write():
title = request.form.get("title", "")
body = request.form.get("body", "")
user = request.form.get("user", "")
info = {
"title": title,
"body": body,
"user": user,
}
return info
@app.route("/")
def index():
return """
<form action="/v1/api/board/write" method="POST">
<input type="text" placeholder="title" name="title"/><br/>
<input type="text" placeholder="body" name="body"/><br/>
<input type="submit"/>
</form>
"""
app.run(host="127.0.0.1", port=8000, debug=True)
먼저 코드 흐름을 살펴보면
index 페이지에서
title, body 제출 폼을 작성하고
/v1/api/board/write 로 보내고
그러면, /v1/api/board/write 여기서
@app.route("/v1/api/board/write", methods=["POST"])
def board_write():
session["idx"] = "guest" .
title = request.form.get("title", "")
body = request.form.get("body", "")
data = f"title={title}&body={body}&user={session['idx']}"
response = requests.post(f"{INTERNAL_API}/board/write", headers=header, data=data) # INTERNAL API 에 이용자가 입력한 값을 HTTP BODY 데이터로 사용해서 요청합니다.
return response.content
보면 form에서 입력값을 get해서
data = f"title={title}&body={body}&user={session['idx']}" 이렇게 문자열로 만들어 /boar/write로 다시 post 요청을 보내고
그럼 /board/write 에서
@app.route("/board/write", methods=["POST"])
def internal_board_write():
title = request.form.get("title", "")
body = request.form.get("body", "")
user = request.form.get("user", "")
info = {
"title": title,
"body": body,
"user": user,
}
return info
이걸 json 형식으로 반환해서
이값이 위 /v1/api/board/write 여기의 response 부분에 응답값으로 가고, 이값을
response.content로 사용자한테 브라우저에 반환해주는 흐름이다.
이 흐름에서 ssrf취약점은
data = f"title={title}&body={body}&user={session['idx']}"
이부분이다.
내부 API로 보낼 Body 데이터를 문자열로 만들어버리는 부분이다.
문자열로 만들어서 보내기 때문에
title값에 test&user=admin 이런식으로 작성해서 폼을 제출해버리면,
title=test&user=admin&body=test&user=guest 이렇게 만들어지고,
& 문자가 파라미터 구분자로 해석돼서
결국
title = test
body=test
user=admin (보통 첫번째 값을 가져오기 때문,,)
요렇게 변조할 수 있음
실제로 해보면
정상적으로 입력하면


괜찮은데 만약에
이렇게 의도적으로 값을 넣어버리면


값이 변조되는 걸 확인 할 수 있다
결론적으로, 위 취약점은 URL조작이 아니라,
내부 API로 전달되는 Body 파라미터 구조를 조작하는 케이스이다.
정리를 해보면,
SSRF를 분석할 때, 결국 사용자의 입력이 서버의 요청 구조 어디에 들어가는지?를 보는 것이 핵심인 것 같다.
URL, path, query, body 어디든 검증 없이 들어간다면 서버가 의도하지 않은 요청을 보내 위협이 발생할 수 있다.
'web2' 카테고리의 다른 글
| docker 사용하기 (run, compose) (0) | 2026.06.17 |
|---|---|
| CSRF(Cross-Site Request Forgery) (0) | 2026.06.04 |
| DOM-based XSS (0) | 2026.06.02 |
| 웹은 어떻게 동작하나요(HTML,JS,CSS,브라우저의 관계) (0) | 2026.05.31 |
| XSS (cross-site scripting) (0) | 2026.05.30 |