TL;DR

  • safe-fetch는 파이썬 표준 라이브러리만 사용하는 단일 파일 URL 가져오기 도구로, DNS 조회 결과와 모든 리디렉션 주소를 검사해 SSRF 위험을 차단함.
  • IPv4·IPv6의 사설, 루프백, 링크 로컬, 멀티캐스트, 예약 및 미지정 주소를 차단하며, IPv4 매핑 IPv6 주소도 검사함.
  • HTTP와 HTTPS만 허용하고 사용자 인증 정보·쿼리 문자열·프래그먼트를 제거하며 응답 본문 크기를 제한함.
  • DNS 검사와 실제 연결 사이의 DNS 리바인딩, 서버 자체의 공인 IP, 제삼자 대상 남용 및 프록시화는 막지 못함.
  • 설치 패키지 없이 safe_fetch.py를 복사해 사용할 수 있으며, Python 3.7 이상이 필요함.

Safe-fetch

  • safe-fetch는 사용자가 제공한 URL을 가져오는 파이썬 도구로, SSRF 취약점이 생기지 않도록 설계됐으며 표준 라이브러리만 사용하고 의존성이 없는 단일 파일로 제공됨.
  • fetch("https://example.com")은 응답의 상태 코드와 content-type 헤더를 돌려주며, 거부되거나 실패한 경우 SafeFetchError를 발생시킴.

문제

  • 웹훅 테스터, 링크 미리보기, 아바타 가져오기, RSS 리더, 웹사이트 확인 도구, 이미지 프록시처럼 애플리케이션이 사용자 입력 URL을 가져오는 경우, 입력값을 그대로 requests.get(user_supplied_url)에 전달하면 위험함.
  • 공격자가 http://169.254.169.254/latest/meta-data/를 입력하면 AWS, GCP, Azure의 클라우드 인스턴스 메타데이터 서비스에 접근할 수 있음. 구성에 따라 이 서비스는 IAM 자격 증명을 반환하며, 공격자는 접근할 수 없더라도 서버가 대신 요청하고 응답을 돌려주면 이를 얻을 수 있음.
  • 이는 OWASP Top 10에 포함된 서버 측 요청 위조(SSRF)이며, 같은 방식으로 127.0.0.1:6379의 Redis, 192.168.x.x 대역의 내부 네트워크 자원, 라이브러리가 해당 스킴을 따르는 경우 file:///etc/passwd에도 접근할 수 있음.
  • 파이썬 표준 라이브러리는 지정한 주소 어디든 기본적으로 연결하며, 이를 막는 기능이 없음.

이 도구의 기능

  • http://169.254.169.254/latest/meta-data/처럼 내부 주소로 연결되는 요청은 해당 주소가 사설 또는 내부 주소로 확인돼 SafeFetchError로 거부됨.
  • 연결 전에 호스트 이름을 해석하고 반환된 모든 주소를 검사함. 공격자가 DNS를 제어해 공인 주소와 사설 주소를 함께 반환하는 경우에도 첫 주소만 검사하는 실수를 피함.
  • IPv4와 IPv6의 사설, 루프백, 링크 로컬, 멀티캐스트, 예약 및 미지정 대역을 차단하며 IPv4 매핑 IPv6 주소도 포함함.
  • 각 리디렉션 단계마다 주소를 다시 검증함. 사용자가 입력한 공인 URL만 확인하는 구현은 그 URL이 302 Location: http://169.254.169.254/로 리디렉션될 때 우회될 수 있음.
  • http와 https만 허용하며, SSRF 악용 경로가 될 수 있는 file://, gopher://, dict://는 거부함.
  • 요청을 보내기 전에 URL에 포함된 인증 정보, 쿼리 문자열 및 프래그먼트를 제거함.
  • 악성 서버가 대용량 응답을 계속 보내는 상황을 막기 위해 응답 본문 크기를 제한함.

설치

API

  • fetch(url, ...)는 다음 인자를 받음: timeout=8, max_redirects=5, max_body=65536, user_agent="safe-fetch/1.0.0", headers=None.
  • 반환 딕셔너리의 항목은 다음과 같음.
  • url: 리디렉션 후 최종 URL
  • status: HTTP 상태 코드
  • headers: 키가 소문자로 변환된 응답 헤더
  • body: max_body 크기에서 잘릴 수 있는 바이트 형식 응답 본문
  • chain: 방문한 URL을 순서대로 담은 목록
  • elapsed_ms: 최종 요청에 걸린 시간
  • 거부되거나 실패한 요청에는 SafeFetchError를 발생시키며, 네트워크 조건으로 인해 다른 예외를 발생시키지 않으므로 하나의 except로 처리할 수 있음.
  • is_blocked_ip(ip_str)는 사용자 대신 연결해서는 안 되는 주소인지 True 또는 False로 반환하며, 자체 연결 구현에서도 사용할 수 있음.
  • resolve_and_check(hostname)은 호스트 이름을 해석해 주소 목록을 반환함. 해석된 주소 중 내부 주소가 있으면 SafeFetchError를 발생시킴.
  • normalise_url(raw)은 스킴을 검증하고 인증 정보·쿼리 문자열·프래그먼트를 제거하며, 스킴이 없는 호스트 이름에는 https://를 추가함. 안전한 URL로 만들 수 없으면 SafeFetchError를 발생시킴.

보호하지 않는 항목

  • 보호 범위를 솔직하게 밝히는 일이 기능 목록을 늘리는 것보다 중요함.
  • DNS 리바인딩은 검사와 연결 사이의 시간 간격 때문에 완전히 차단되지 않음. 공격자가 제어하는 DNS 서버가 검사 시점에는 공인 주소를 반환하고, TTL을 1초로 설정해 연결 시점에는 사설 주소를 반환할 수 있음.
  • 이를 완전히 막으려면 DNS를 한 번만 조회하고 해당 IP에 고정 연결한 뒤, TLS SNI와 Host 헤더에는 호스트 이름을 별도로 전달해야 함. urllib에서 이를 구현하는 것은 개입 범위가 커서 이 라이브러리는 해당 방식을 사용하지 않음.
  • 대부분의 애플리케이션에는 이 라이브러리의 검사가 비례적인 대응이지만, 실제로 적대적인 입력을 처리한다면 IP 주소를 고정해야 함.
  • 서버에 공인 IP가 있고 그 주소에서 접근되면 안 되는 서비스도 운영한다면, 주소가 사설이 아니므로 이 도구가 차단하지 못함. 방화벽도 함께 사용해야 함.
  • 이 도구는 서버가 내부 주소에 접근하는 일을 막지만, 제삼자 서버를 과도하게 요청하는 애플리케이션 수준의 악용은 막지 않음. 요청 제한을 추가해야 함.
  • 가져온 본문을 사용자에게 그대로 반환하면 이 검사 기능을 적용해도 공개 프록시가 될 수 있음. 필요한 내용만 반환해야 함.

테스트

  • python tests/test_safe_fetch.py 명령으로 테스트를 실행할 수 있으며, pytest는 필수가 아니지만 함께 사용할 수도 있음.
  • 테스트는 차단 대상 전체 대역, IPv4 매핑 IPv6, 스킴 거부, 인증 정보 제거, 이름과 주소를 통한 내부 대상 요청 거부를 검사함.

제작 배경

  • 이 도구는 무료 보안 도구 모음인 IsSiteSafe를 구동하는 스캐너에서 분리해 만든 모듈임.
  • 해당 사이트는 누구나 URL을 입력하면 서버가 가져오는 방식으로 작동하며, 이것이 이 모듈이 안전하게 만들려는 바로 그 상황임.

라이선스

  • MIT 라이선스이며 자유롭게 사용할 수 있음.