SSRF、密钥与 PII 保护
正在读取学习记录…
资料 Agent 接受一个文档地址,服务端随后准备读取文档。正常业务是访问已批准的资料站点。SSRF 是 Server-Side Request Forgery 的缩写,意思是输入使服务端代替用户向不该访问的位置发请求。Agent 的网页读取工具天然处在这个入口。
密钥是服务用于认证的秘密值;PII 表示可识别个人的信息,例如邮箱、电话号码及与其他数据组合后能识别个人的字段。即使资料读取地址合规,服务也要避免把密钥和无关个人信息送进模型、输出或日志。
先观察一次正常策略判断:https://docs.example.test/guide 是批准的资料域名,固定地址表返回一个公网地址,程序判定地址策略通过。该地址表是本地夹具,后面的操作不会打开网页。
地址检查放在请求之前,数据裁剪放在输出之前
URL 解析将协议、主机、端口和用户凭据拆开;用字符串“包含域名”来判断,容易把子串误认为可信主机。程序只允许 HTTPS、已批准的完整主机名和默认 HTTPS 端口,并拒绝 URL 自带凭据。
DNS 是把主机名转换成地址的系统。一个主机可能同时返回 IPv4、IPv6 或多个地址,所以 check_url 检查固定表中的全部地址;只要存在非公网地址就拒绝。IPv4 的回环地址 127.0.0.1 和 IPv6 的 ::1 都指向本机,无法作为公开资料地址通过。
真正网络客户端还要把检查过的解析结果与实际连接绑定,防止检查之后重新解析到另一地址;HTTPS 的证书和主机校验也要继续执行。每次重定向产生新地址时重新验证或禁用重定向,并通过网络出口策略限制到内网、元数据服务等位置的连接。
| 边界 | 本地夹具怎样判断 | 接入真实读取工具时的位置 |
|---|---|---|
| 协议与端口 | HTTPS,443或默认端口 | URL解析后 |
| 完整域名 | 可信集合成员 | 构造请求前 |
| 全部解析地址 | 都是公网地址 | DNS结果与连接绑定处 |
| 新跳转地址 | 重新跑相同策略 | 跟随每次重定向前 |
| 输出字段 | 保留 title | 送模型或记录日志前 |
在空目录保存并运行两个文件
在 PowerShell 输入以下命令创建练习目录,并进入目录。目录创建好后,在编辑器中新建下面两份文件并保存到这个目录。
New-Item -ItemType Directory -Force l8-ssrf
Set-Location l8-ssrfmacOS 终端:
mkdir -p l8-ssrf
cd l8-ssrf先保存 ssrf_demo.py。它包含前面解释的全部地址以及正常和边界输入。
"""用固定地址表做 URL 策略判断,不发起网络请求。"""
import ipaddress
from urllib.parse import urlsplit
# 地址表模拟解析结果,包含同一域名返回多个地址的情况。
DNS = {"docs.example.test": ["93.184.216.34"],
"mixed.example.test": ["93.184.216.34", "127.0.0.1"]}
HOSTS = {"docs.example.test", "mixed.example.test"}
TARGET = "https://docs.example.test/guide"
def check_url(url, dns):
try:
parsed = urlsplit(url)
port = parsed.port
except ValueError:
return "拒绝:URL格式"
if parsed.scheme != "https":
return "拒绝:协议"
if parsed.username is not None or parsed.password is not None:
return "拒绝:URL含凭据"
if parsed.hostname not in HOSTS or port not in (None, 443):
return "拒绝:域名或端口"
addresses = dns.get(parsed.hostname, [])
if not addresses:
return "拒绝:无解析地址"
try:
# 一个解析地址违规就拒绝,不能只挑第一个公网地址。
if any(not ipaddress.ip_address(ip).is_global for ip in addresses):
return "拒绝:非公网地址"
except ValueError:
return "拒绝:地址格式"
return "允许:地址策略通过"
def safe_fields(record):
# 按字段白名单输出,测试邮箱与测试令牌都留在输入侧。
return {key: record[key] for key in ("title",) if key in record}
def main():
print("资料地址 ->", check_url(TARGET, DNS))
print("混合解析 ->", check_url("https://mixed.example.test/guide", DNS))
print("最少字段 ->", safe_fields(
{"title": "接口文档", "email": "reader@example.test", "token": "FAKE_TOKEN"}))
if __name__ == "__main__":
main()再保存 test_ssrf.py。测试通过程序的公开函数检查正常路径与拒绝路径;测试文件和主程序放在同一目录,才能按文件名导入。
import unittest
# 固定良性输入检查正常、拒绝和边界路径,不连接外部服务。
from ssrf_demo import DNS, check_url, safe_fields
class AddressTests(unittest.TestCase):
def test_normal(self):
self.assertTrue(check_url("https://docs.example.test/guide", DNS).startswith("允许"))
def test_mixed_addresses(self):
self.assertEqual(check_url("https://mixed.example.test/", DNS), "拒绝:非公网地址")
def test_scheme(self):
self.assertEqual(check_url("file:///guide", DNS), "拒绝:协议")
def test_credentials(self):
self.assertEqual(check_url("https://u:p@docs.example.test/", DNS), "拒绝:URL含凭据")
def test_host_and_port(self):
for url in ("https://other.example.test/", "https://docs.example.test:8443/"):
self.assertEqual(check_url(url, DNS), "拒绝:域名或端口")
def test_dns_boundary(self):
for addresses in ([], ["::1"], ["bad-ip"]):
self.assertTrue(check_url("https://docs.example.test/",
{"docs.example.test": addresses}).startswith("拒绝"))
def test_field_projection(self):
self.assertEqual(safe_fields({"title": "指南", "token": "FAKE", "email": "a@test"}),
{"title": "指南"})
if __name__ == "__main__":
unittest.main()返回同一个终端,先列出当前目录内容,确认两个文件名都以 .py 结尾,然后运行主程序。
ls
python -X utf8 ssrf_demo.py程序输出:
资料地址 -> 允许:地址策略通过
混合解析 -> 拒绝:非公网地址
最少字段 -> {'title': '接口文档'}资料地址通过策略,混合解析由于包含本机地址被拒绝。最后一行只留下标题;测试邮箱和 FAKE_TOKEN 没有进入输出。这是按字段结构选择数据,适合已知记录格式;自由文本中的个人信息还需要相应识别、脱敏策略和样本验证。
运行测试:
python -X utf8 -m unittest -v test_ssrf.py末尾应显示 Ran 7 tests 和 OK。若提示找不到模块,先用 dir 核对目录与文件名。
改一处,观察决定怎样变化
把 TARGET = "https://docs.example.test/guide" 改成 TARGET = "https://docs.example.test:8443/guide",运行主程序。第一行变为 资料地址 -> 拒绝:域名或端口,其余两行不变。将端口删除恢复初始地址。
python -X utf8 ssrf_demo.py恢复原值后,重新运行测试。
接到生产客户端时容易漏在哪里
| 现象 | 重点检查 | 修正动作 |
|---|---|---|
| 检查通过,实际连接进入内网 | 是否二次DNS解析 | 将连接绑定到受检地址,核对客户端与代理行为 |
| 初始地址正常,跳转后不正常 | 重定向配置 | 禁用自动跳转或逐跳校验 |
| 域名看着一样却被拒绝 | 大小写、端口和完整主机名 | 统一解析,按完整主机名维护名单 |
| 日志带测试令牌 | 是否记录整包输入 | 白名单选字段,单独管理秘密和调试日志 |
业务必须访问公司内网时,应给该工具独立的目标服务名单和网络身份,避免与“读取任意公开网页”的工具混用权限。密钥从受控秘密服务注入到真正需要的调用端,模型通常只需工具名称和参数格式。错误堆栈、URL查询参数和代理日志也要检查是否泄漏秘密。
OWASP SSRF 防护指南区分固定目标与任意外部目标,并讨论应用层与网络层检查。该离线程序展示策略分支,真实DNS、TLS、重定向和网络出口行为需在接入的客户端上验证。
课后练习展开习题收起习题
正在读取本地记录…
不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。
选择题 · 先判断边界
关于“SSRF、密钥与 PII 保护”,哪种做法更稳妥?
对应知识点:L8-ssrf
简答题 · 解释失败
写出一个失败输入并说明记录字段。
对应知识点:L8-ssrf
代码题 · 边界测试
补充正常、边界、失败三组输入和预期结果。
对应知识点:L8-ssrf
场景题 · 结果未知
证据不足时如何设计状态和人工接管?
对应知识点:L8-ssrf
描述会发送给本站维护者,用于修订文章,保存90天。请只填写文章问题,省略个人信息、密钥和内部资料。