설명
서버 사이드 요청 위조(SSRF)는 서버가 외부로 보내는 HTTP 요청의 주소(URL)나 Host를 사용자 입력으로 그대로 사용해서 발생하는 취약점입니다. 공격자는 요청 파라미터나 헤더 등에 악의적인 URL을 넣어 서버에게 대신 요청을 보내게 만들 수 있습니다. 이 때 서버는 루프백(127.0.0.1), 메타데이터 엔드포인트, 사설 IP이나 원래는 외부와 통신하면 안 되는 서비스로 요청을 보낼 수 있어, 인증 우회, 내부 시스템 정보 조회, 포트 스캐닝 등 다양한 공격이 가능합니다.
잠재적 영향
- 내부 네트워크 접근 (내부망 무단 접근): 서버가 있는 네트워크 내부(사설 IP, 내부 API, DB 관리 콘솔 등)에 사용자가 서버를 통해 요청을 보낼 수 있습니다. 이를 통해 내부 시스템 구조 파악, 추가 공격(예: RCE로 이어지는 취약 API 호출) 등이 가능합니다.
- 민감 정보 유출 (내부 정보 노출): 클라우드 메타데이터 엔드포인트(예: AWS 169.254.169.254)나 내부 설정 서버에 요청을 보내 토큰, 크레덴셜, 설정 파일 등의 민감 정보를 획득할 수 있습니다.
- 서비스 악용 및 우회 (보안 정책 우회): 원래는 방화벽, IP 기반 ACL, WAF 등으로 막혀 있어야 하는 대상에게, 애플리케이션 서버를 프록시처럼 이용해 요청을 보낼 수 있어 보안 정책이 우회됩니다.
- 서비스 장애 유발 (서비스 거부): 의도적으로 느리거나 응답이 없는 주소, 매우 큰 응답을 보내는 서버로 요청을 반복해 보내 서버 자원을 소모시키고, 결과적으로 서비스 장애(DoS)를 일으킬 수 있습니다.
해결 방법
- 외부 요청 대상은 코드에서 정한 도메인·호스트 허용 목록으로 제한하세요.
- 필요한
http/https스킴과 포트만 허용하고, 다른 스킴은 거부하세요. - 실제 IPv4·IPv6 연결 주소에서 루프백, 사설, 링크 로컬, 메타데이터 등 허용하지 않는 대역을 제한하세요. DNS 재바인딩을 막으려면 검증한 주소를 실제 연결에도 사용해야 합니다.
- 전체 URL 대신 서버에서 정한 고정 대상의 식별자를 받으세요. 경로나 쿼리가 필요하면 허용한 형태로 제한하고 적절히 인코딩하세요.
- 타임아웃, 리다이렉트 정책과 응답 크기 상한을 적용하세요.
- SSRF 우회 입력을 시험하고 의심스러운 아웃바운드 요청을 모니터링하세요.
예시
변경 전
go
package main
import (
"io"
"log"
"net/http"
)
// 비안전한 SSRF 예제
func proxyHandler(w http.ResponseWriter, r *http.Request) {
// 사용자 입력으로부터 전체 URL을 그대로 받음
target := r.URL.Query().Get("url")
if target == "" {
http.Error(w, "missing url", http.StatusBadRequest)
return
}
// 취약: 사용자가 준 URL로 서버가 직접 요청을 보냄
// 예: http://myserver/proxy?url=http://169.254.169.254/latest/meta-data/
resp, err := http.Get(target)
if err != nil {
http.Error(w, "request failed", http.StatusBadGateway)
return
}
defer resp.Body.Close()
// 단순히 결과를 그대로 전달
w.WriteHeader(resp.StatusCode)
if _, err := io.Copy(w, resp.Body); err != nil {
log.Println("write response error:", err)
}
}
func main() {
http.HandleFunc("/proxy", proxyHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
변경 후
go
package main
import (
"io"
"log"
"net/http"
"time"
)
var outboundClient = &http.Client{
Timeout: 5 * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
// 허용된 호스트가 내부 주소나 다른 호스트로 리다이렉트하지 못하게 합니다.
return http.ErrUseLastResponse
},
}
func safeProxyHandler(w http.ResponseWriter, r *http.Request) {
endpointID := r.URL.Query().Get("endpoint")
var target string
// 외부 입력은 URL이 아니라 서버가 관리하는 고정 엔드포인트만 선택합니다.
switch endpointID {
case "users":
target = "https://api.example.com/v1/users"
case "status":
target = "https://status.example.com/api/status"
default:
http.Error(w, "unknown endpoint", http.StatusBadRequest)
return
}
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, target, nil)
if err != nil {
http.Error(w, "request build failed", http.StatusInternalServerError)
return
}
resp, err := outboundClient.Do(req)
if err != nil {
http.Error(w, "request failed", http.StatusBadGateway)
return
}
defer resp.Body.Close()
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(resp.StatusCode)
const maxResponseBytes = 1024 * 1024
if _, err := io.Copy(w, io.LimitReader(resp.Body, maxResponseBytes)); err != nil {
log.Println("write response error:", err)
}
}
func main() {
http.HandleFunc("/proxy", safeProxyHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
설명:
- 변경 전: 이 예시에서는 사용자가 query parameter "url"로 보낸 값을 아무 검증 없이 http.Get에 그대로 전달합니다. 이 경우 공격자는 내부망이나 클라우드 메타데이터 엔드포인트 같은 민감한 주소를 넣어 서버에게 대신 요청을 보내게 만들 수 있습니다. 서버는 네트워크 상에서 더 신뢰받는 위치에 있기 때문에, 원래는 외부에서 직접 접근할 수 없는 시스템에 접근이 가능해지고, 그 응답 내용을 공격자에게 그대로 전달할 수 있어 SSRF 취약점이 발생합니다.
- 변경 후: 다음과 같이 SSRF 위험을 줄입니다.
- 사용자 입력은 URL이나 경로가 아니라 서버가 관리하는 고정 엔드포인트의 ID만 선택합니다.
- 실제 요청 URL은 코드에 정의된 고정 분기에서 선택하므로 외부 입력이 스킴·호스트·포트·경로를 바꿀 수 없습니다.
CheckRedirect가 리다이렉트를 따르지 않도록 하여 허용된 외부 서버가 내부 주소로 요청을 우회시키는 것을 막습니다.- 요청 타임아웃과 응답 크기 상한을 적용해 느리거나 큰 응답에 의한 자원 고갈을 제한합니다.
- 업스트림 콘텐츠를 애플리케이션 출처의 HTML로 해석하지 않도록 이진 콘텐츠 타입과
nosniff를 강제합니다. 고정된 대상의 DNS와 서비스도 신뢰할 수 있어야 하며, 필요한 아웃바운드 네트워크 제한을 별도로 적용하세요. DNS/IP 검사를 사용하는 더 동적인 설계라면 검증한 IP가 실제 연결에도 사용되도록 커스텀DialContext에 결합해야 합니다. 별도LookupIP결과만 확인한 뒤 기본 클라이언트로 다시 해석하면 DNS 재바인딩을 막을 수 없습니다.