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에 포함된 인증 정보, 쿼리 문자열 및 프래그먼트를 제거함.
- 악성 서버가 대용량 응답을 계속 보내는 상황을 막기 위해 응답 본문 크기를 제한함.
설치
- 패키지 설치는 필요하지 않으며, 의존성도 없음.
safe_fetch.py파일을 프로젝트에 복사하면 됨. - 파일 다운로드 주소: https://raw.githubusercontent.com/Rehanfaisal/ssrf-safe-fetch/main/safe_fetch.py
- Python 3.7 이상이 필요함.
API
fetch(url, ...)는 다음 인자를 받음:timeout=8,max_redirects=5,max_body=65536,user_agent="safe-fetch/1.0.0",headers=None.- 반환 딕셔너리의 항목은 다음과 같음.
url: 리디렉션 후 최종 URLstatus: 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 라이선스이며 자유롭게 사용할 수 있음.
댓글 (0)
로그인하면 이 기사에 내 생각을 남길 수 있어요