必须对url做协议白名单校验后再传入curl_init(),仅允许http和https协议;需用parse_url()解析并转小写校验scheme,禁止file、gopher等危险协议;同时校验host是否为公网ip,禁用curlopt_followlocation防止跳转绕过。

curl_init() 传入 URL 前必须做协议白名单校验
直接把 $_GET['url'] 塞进 curl_init() 是高危操作,curl 默认支持 http、https、file、dict、gopher 等 28 种协议,攻击者用 gopher:// 打 Redis、用 file:///etc/passwd 读文件,一步到位。
正确做法是先用 parse_url() 拆解 URL,再严格校验 scheme:
-
parse_url()返回数组,$url_info['scheme']是协议名,注意要转小写(strtolower($URL))再判断 - 只允许
http和https,其他一律die()或抛异常,别用黑名单(比如只拦file),因为新协议随时可能被支持 - 不要依赖
filter_var($url, FILTER_VALIDATE_URL),它对gopher://127.0.0.1:6379/这类也返回 true
host 解析不能只比字符串,得走 DNS 或 IP 转换
很多人用 in_array($url_info['host'], ['127.0.0.1', 'localhost']) 拦内网,但这是无效的——localhost 可能解析成 ::1,127.0.0.1 可被 127.0.0.1.nip.io 绕过,更别说十六进制或八进制 IP(如 0x7f000001)。
真正靠谱的方式是:
- 用
gethostbyname()获取 host 对应的 IPv4 地址,再用ip2long()转整型,判断是否在私有网段范围内(0x7f000000~0x7fffffff、0xac100000~0xac1fffff等) - 或者用
filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)直接校验公网 IP - 如果业务真要访问内网服务,白名单必须是具体 IP + 端口组合,不能只放
127.0.0.1
curl_setopt() 别漏掉 CURLOPT_FOLLOWLOCATION 和 CURLOPT_MAXREDIRS
攻击者常利用 302 跳转绕过前面的校验:先请求一个合法域名,再跳到 file:/// 或内网地址。默认 CURLOPT_FOLLOWLOCATION 是开启的,且没限制跳转次数。
必须显式关闭或设限:
-
curl_setopt($CH, CURLOPT_FOLLOWLOCATION, false)—— 最简单粗暴 - 如果必须支持跳转,加
curl_setopt($CH, CURLOPT_MAXREDIRS, 2)并在每次跳转后重新校验新 URL 的scheme和host - 同时关掉
CURLOPT_UNRESTRICTED_AUTH,避免跳转后把原始认证头带到内网请求里
file_get_contents() 的防护逻辑和 curl 几乎一致
别以为不用 curl 就安全——file_get_contents() 同样支持 file://、php://、data:// 等协议,且不走 curl_setopt() 那套配置。
防护要点一样硬核:
- 同样要
parse_url()校验scheme,只留http和https - 禁用
allow_url_fopen = Off是根本解法,但会影响现有逻辑,得评估兼容性 - 如果必须开,至少配合
stream_context_create()设置超时和 DNS 超时,防止 SSRF 引发 DoS
最易被忽略的是:所有校验必须在 curl_init() 或 file_get_contents() 调用前完成,中间任何字符串拼接、重定向、缓存回写都可能绕过检查。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











