본문 바로가기

프로그래밍/golang

[Golang] net/url 패키지를 활용한 SSRF 방어

 

 

1. SSRF(Server-Side Request Forgery) 란?

SSRF를 번역하면 "서버 측 요청 위조" 이며,

공격자가 서버를 속여서, 서버가 의도하지 않은 주소로 HTTP 요청을 보내게 만드는 공격 방법 입니다.

공격자는 서버의 권한을 이용해서 서버 내부망에 있는 다른 서버나, 서버 자산 등을 접근하려고 시도합니다.

2. SSRF 왜 발생하는가?

서버와 기능을 구현하다보면 서버가 외부 URL에 대신 접근해야하는 기능이 필요할 때가 있습니다.

  • 이미지 프록시/썸네일 생성 : 사용자가 입력한 프로필 이미지 URL을 서버가 다운로드 하여 리사이징 할때
  • 웹훅 알림 : 결제가 완료되면 사용자가 설정한 URL로 서버가 알림을 보낼 때
  • 외부 데이터 가져오기 : RSS 피드나 외부 API 문서를 URL을 통해서 불러올 때

이와 같이 서버가 외부와 통신해야하는 기능이 필요한 상황에서 , 유저가 입력한 URL를 제대로 검증하지 않으면 SSRF의 통로가 됩니다.

 

공격자가 이러한 방법을 통해 내부망으로 침투하여, 서버의 권한으로 관리자 페이지나 데이터베이스를 접근하기도 하며, 포트 스캐닝을 통해서 열린 포트들을 전부 접근해 취약점이 있는지, 공격 지점을 계속 찾습니다.

 

3. 코드 예제

SSRF 방어는 "유저가 입력한 URL을 신뢰하여 바로 사용하지 말자"

각 URL에 포함된 Scheme, Host, IP 대역들을 검사해 안전한 요청을 보장하는 예제 코드입니다.

 

package main

import (
    "errors"
    "fmt"
    "net"
    "net/url"
    "strings"
)

func main() {
    // 테스트 케이스: 다양한 공격 시나리오와 정상 URL
    tests := []string{
        "https://example.com/path",          // 정상 (Allowlist 포함)
        "http://example.com/path",           // scheme 제한 (HTTPS 필수)
        "https://127.0.0.1/admin",           // loopback 차단
        "https://localhost/admin",           // loopback 차단
        "https://169.254.169.254/latest/",   // Cloud Metadata IP 차단
        "https://evil.com@127.0.0.1/admin",  // Userinfo 트릭 차단
    }

    for _, raw := range tests {
        // example.com만 허용하는 화이트리스트 정책 적용
        err := validateOutboundURL(raw, []string{"example.com"})
        fmt.Printf("%s -> %v\n", raw, err)
    }
}

// validateOutboundURL은 URL의 안전성을 검증합니다.
func validateOutboundURL(raw string, allowHosts []string) error {
    u, err := url.Parse(raw)
    if err != nil {
        return err
    }

    // 1. Scheme 제한: 보안을 위해 HTTPS만 허용
    if u.Scheme != "https" {
        return errors.New("scheme not allowed")
    }

    // 2. Userinfo 차단: evil.com@127.0.0.1 형태의 우회 공격 방지
    if u.User != nil {
        return errors.New("userinfo not allowed")
    }

    host := u.Hostname()
    if host == "" {
        return errors.New("missing host")
    }

    // 3. Allowlist(도메인) 검증: 허용된 도메인 리스트인지 확인
    allowed := false
    for _, h := range allowHosts {
        if strings.EqualFold(host, h) {
            allowed = true
            break
        }
    }
    if !allowed {
        return errors.New("host not allowed")
    }

    // 4. IP 직접 입력 차단: 호스트가 IP일 경우 내부망 주소인지 확인
    if ip := net.ParseIP(host); ip != nil {
        if isBadIP(ip) {
            return errors.New("ip not allowed")
        }
    }

    return nil
}

// isBadIP는 루프백, 사설망, 링크 로컬 IP 대역을 필터링합니다.
func isBadIP(ip net.IP) bool {
    // 127.0.0.1 등 루프백이나 멀티캐스트 대역 차단
    if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
        return true
    }

    // RFC1918 IPv4 사설망 대역 차단
    if v4 := ip.To4(); v4 != nil {
        switch {
        case v4[0] == 10: // 10.0.0.0/8
            return true
        case v4[0] == 172 && v4[1] >= 16 && v4[1] <= 31: // 172.16.0.0/12
            return true
        case v4[0] == 192 && v4[1] == 168: // 192.168.0.0/16
            return true
        case v4[0] == 169 && v4[1] == 254: // 169.254.0.0/16 (Link-local/AWS Metadata)
            return true
        }
    }
    return false
}

 

 

  • Userinfo(@) 트릭 차단
    URL 표준(RFC 3986)에서는 "https://ID:Password@Host" 형식도 허용이 됩니다.
    위 형식은 로그인이 필요한 페이지를 접속하기 편하게 하기 위한 규격입니다
    예 ) "https://admin@1234@server.com"

    공격자들은 이러한 구조를 이용해서 앞부분에서는 안전한 주소를 넣고 뒷 부분은 내부망 주소를 넣습니다.
    예) "https://google.com@127.0.0.1/admin"

    이 방법을 통해서 앞쪽 주소에는 안전한 도메인을 넣고 @ 뒤에는 내부 서버를 넣어서 접근을 시도하는것을 방지합니다.


  • Hostname, Host
    u.Host 대신 u.Hostname() 사용을 권장합니다.
    Host는 포트 번호까지 포함 할 수있어 IP검증 시 에러가 날 수 있고, Hostname은 순수 도메인 또는 IP만 반환합니다


  • 169.254.169.254
    이 IP는 AWS, Google Cloud, Azure 같은 클라우드 서비스에서 내부적으로 쓰는 약속된 주소입니다.
    이 주소는 해당 서버의 정보를 알려주는 메타데이터 API 서비스 주소이며,
    서버가 이 주소에 요청을 보내면 현재 서버의 IAM Role의 Access Key, Secret Key 등 민감 자격 증명 정보들을 반환합니다.
    이를 통해서 공격자는 클라우드의 권한정보를 탈취합니다.

    이러한 위험성을 막기 위한 작업들이 필요합니다.
검증 항목 대상 IP 대역 (예시) 차단 이유 (위험성) Go 메서드/로직
Loopback 127.0.0.1, ::1 서버 자체에서 실행 중인 DB, Redis, 관리자 페이지 등에 접근 차단 ip.IsLoopback()
Link-Local 169.254.0.0/16 AWS/GCP 메타데이터 서버에 접근하여 IAM 자격 증명 탈취 차단 ip.IsLinkLocalUnicast()
Private (A) 10.0.0.0/8 기업 내부망에 위치한 다른 서버나 기기로의 접근 차단 v4[0] == 10
Private (B) 172.16.0.0/12 Docker 컨테이너 간 통신망 등 내부 네트워크 침투 방지 v4[0] == 172 && v4[1] >= 16
Private (C) 192.168.0.0/16 일반적인 사내 공유기 대역 및 내부 서비스 노출 방지 v4[0] == 192 && v4[1] == 168
Multicast 224.0.0.0/4 내부망의 다른 장비들을 탐색(Scanning)하려는 시도 차단 ip.IsLinkLocalMulticast()