kylemillerbuilds/safe-url-fetch

GitHub: kylemillerbuilds/safe-url-fetch

该模块通过 Scheme 白名单、全局 IP 校验、逐跳重定向验证和资源限制,在服务端请求用户提供的 URL 时防止 SSRF 漏洞。

Stars: 1 | Forks: 0

# safe-url-fetch 在你的服务器上请求用户提供的 URL,而不会产生 SSRF 漏洞。只需一个小型 Python 模块,无需维护“恶意 IP”列表,并且测试套件无需网络即可运行。每一项检查的存在都有其理由,因为一旦遗漏,内部系统就会暴露给外部访问。

How safe-url-fetch judges a URL before the server connects

## 背景 我运营的一家 SaaS 产品允许用户粘贴他们的网站地址,以便应用获取他们的 Logo 和品牌颜色。当服务器去请求用户输入的 URL 的那一刻,这个请求就变成了一种武器。如果指向云主机上的 `http://169.254.169.254/`,它就会读取实例凭证;如果指向 `http://localhost:6379`,它就会与内部的 Redis 通信;如果指向 `http://10.0.0.5/admin`,它就会触达私有网络中的邻居节点。用户永远看不到这些地址,是服务器在建立连接,因此消耗的是服务器的信任度。 这类漏洞就是 SSRF(服务器端请求伪造),这也是应用的内部系统受到外部访问的最常见方式之一。这个模块就是我为每一个接收人工输入 URL 的请求所设置的前置防护。 ## 包含内容 ``` safe_url_fetch.py the guard: validate_url() and fetch() test_safe_url_fetch.py 18 tests, no network (DNS and HTTP are faked) requirements.txt requests, and nothing else ``` ## 用法 ``` from safe_url_fetch import fetch, validate_url, SafeURLError # 仅验证(您自行获取): try: validate_url(user_supplied_url) except SafeURLError as e: return reject(str(e)) # 验证并获取,在每个 hop 重新检查 redirects: try: body = fetch(user_supplied_url, max_bytes=2_000_000, timeout=5) except SafeURLError as e: return reject(str(e)) ``` `fetch` 以 `bytes` 格式返回请求体,或者抛出 `SafeURLError` 异常。绝不会将任何不安全的内容作为结果返回。 ## 强制执行的四项规则 1. **Scheme 白名单:仅允许 http 和 https。** 仅此一项即可拦截 `file:///etc/passwd`、`gopher://` 和 `data:` 载荷。 2. **解析主机映射到的每一个 IP,如果其中任何一个不是全局可路由的,则拒绝请求。** 检查使用的是 `ipaddress.ip_address(ip).is_global`,而不是手动编写的私有范围列表。这个单一的标准库调用还会拒绝运营商级 NAT (`100.64.0.0/10`)、`0.0.0.0/8`、链路本地地址 (`169.254.0.0/16`,即云元数据范围) 以及 IPv6 唯一本地地址,并且无需进行任何更新维护。检查 *每一个* DNS 响应,而不仅仅是第一个,可以击败那种一条记录公开而另一条记录内部的“水平分割”伎俩。 3. **手动处理重定向并重新验证每一跳。** 一个返回 `302 Location: http://169.254.169.254/` 的公开 URL 是教科书式的一次性检查绕过方式。在这里,`allow_redirects` 被关闭,每一个目标在建立连接之前都会经过完整的验证。 4. **限制超时时间、响应大小和重定向次数。** 大小限制会在数据流式传入时统计字节数,因为 `Content-Length` 头部只是一个声明,而不是保证。 ## 关键设计决策 **始终闭环失败。** 无法解析的 IP、无法解析的主机、没有主机的 URL:每一个不确定的情况都会引发 `SafeURLError`。这与我发布的另一个代理防护工具正好相反,并且出于完全相反的原因它是正确的。一项你无法证明其已通过的安全检查,就等于没有通过。 **使用 `is_global` 而不是黑名单。** 手工编写的私有 IP 范围列表很容易过时,并且总是会遗漏一些东西。CGNAT 和 `0.0.0.0/8` 是最常被遗漏的两个网段。依赖 `ipaddress` 意味着由标准库来维护这个列表。 **让解析器进行规范化。** `http://2130706433/` 是一种完全合法的 `http://127.0.0.1/` 的写法。与其针对十进制、八进制和十六进制的 IP 字面量进行特殊处理,防护机制不如将主机交给 `getaddrinfo`,并判断它最终解析出的任何结果。混淆在解析后就会被拦截。 **客观存在的残余风险。** 验证阶段会解析主机,然后 `requests` 会在连接时再次解析它。一个 TTL 为 0 的 DNS 讔回可以在验证阶段回答“公开”,而在一毫秒后的连接阶段回答“私有”。该模块有意接受了这一空档,并且只有在请求体不会交回给不受信任的调用者时,接受这种空档才是安全的。在最初为其构建的获取 Logo 和颜色的使用场景中,一次成功的内部命中只会返回一个近乎为空的指示,因此实际的可利用性几乎为零。如果你将完整的响应体返回给不受信任的用户,请通过固定已验证的 IP 并使用自定义传输适配器直接连接到该 IP 来消除这个空档。这种权衡已经写在模块的 docstring 中,这样下一个人继承的就不仅仅是代码,还有其背后的设计逻辑。 ## 测试 ``` python3 test_safe_url_fetch.py ``` 无需网络,也不需要第三方包:DNS 使用伪造的解析器,HTTP 是脚本化模拟的,并且重定向到内部的绕过方式已被直接测试。这 18 个测试用例涵盖了 IP 分类(包括 IPv4 映射的 IPv6 和无法解析的输入)、Scheme 白名单、水平分割主机、相对和绝对重定向、重定向循环以及流式大小限制。 ## 作者 Kyle Miller。我致力于构建运行真实业务的 AI 系统和定制软件:电商自动化、内部工具、内容引擎。这个仓库是从我的一个生产环境应用中提取并清理出来的一部分。业务逻辑保留在内部不公开。 **雇佣我:** [themisfoundry.com](https://themisfoundry.com) ## 许可证 MIT
标签:API密钥检测, Python, SSRF防护, Web安全, 安全防御库, 无后门, 网络请求, 蓝队分析, 输入验证, 逆向工具