PHP 怎么PHP SSRF 防御

wen PHP项目 1

本文目录导读:

PHP 怎么PHP SSRF 防御

  1. 白名单机制(最有效)
  2. IP 地址验证与过滤
  3. 禁用重定向跟随
  4. 使用 parse_url 二次验证
  5. 对 cURL 进行精细化配置
  6. 网络层限制(最底层防御)
  7. 防御 DNS 重绑定攻击
  8. 综合防御示例
  9. 总结防御要点

在 PHP 中防御 SSRF(Server-Side Request Forgery,服务端请求伪造)攻击,核心思路是 严格限制服务端发起的网络请求,防止攻击者利用漏洞访问内部网络或恶意外部资源。

以下是 PHP 防御 SSRF 的 7 个核心策略,按推荐程度排序:

白名单机制(最有效)

只允许访问预定义的、受信任的主机或 IP 地址。

<?php
function safeCurl($url) {
    $whitelist = [
        'api.trusted-service.com',
        'www.example.com',
        '10.0.0.1'  // 仅限特定内部服务
    ];
    $host = parse_url($url, PHP_URL_HOST);
    if (!in_array($host, $whitelist)) {
        throw new Exception("Host not allowed");
    }
    // 执行请求...
}

IP 地址验证与过滤

过滤私有 IP 地址(如 10.x.x.x、172.16-31.x.x、192.168.x.x)和保留地址(127.0.0.1、0.0.0.0)。

<?php
function isPrivateIp($ip) {
    $privateRanges = [
        '10.0.0.0' => '8',
        '172.16.0.0' => '12',
        '192.168.0.0' => '16',
        '127.0.0.0' => '8',      // loopback
        '169.254.0.0' => '16',   // link-local
        '0.0.0.0' => '8'         // 通常表示所有接口
    ];
    foreach ($privateRanges as $range => $mask) {
        if (ipInRange($ip, $range, $mask)) {
            return true;
        }
    }
    return false;
}
function safeRequest($url) {
    $host = parse_url($url, PHP_URL_HOST);
    // 先解析为 IP(防止 DNS 欺骗,多次解析确认)
    $ip = gethostbyname($host);
    if (isPrivateIp($ip)) {
        throw new Exception("Access to internal IP denied");
    }
    // 额外检查 IPv6 的私有地址(fd00::/8 等)
    if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
        $ipv6 = inet_pton($ip);
        if (strlen($ipv6) >= 8 && ($ipv6[0] & 0xfe) === 0xfc) {
            throw new Exception("Private IPv6 denied");
        }
    }
    // 执行 cURL 请求...
}

禁用重定向跟随

攻击者可能利用 HTTP 重定向将请求引导到内部资源。

<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);  // 关闭自动跟随重定向
// 或者手动处理重定向并验证每个跳转的目标

更好的做法是拦截重定向并重新验证:

<?php
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_MAXREDIRS, 5);  // 限制重定向次数
// 或者使用 CURLOPT_PROTOCOLS 限制协议

使用 parse_url 二次验证

攻击者可能利用 URL 解析差异绕过验证(如 http://evil.com\@internalhttp://evil.com#@internal)。

<?php
function validateUrl($url) {
    $parsed = parse_url($url);
    // 检查是否包含用户信息(@ 符号前面部分)
    if (isset($parsed['user']) || isset($parsed['pass'])) {
        throw new Exception("URL with credentials not allowed");
    }
    // 检查 scheme
    $allowedSchemes = ['http', 'https'];
    if (!in_array($parsed['scheme'], $allowedSchemes)) {
        throw new Exception("Scheme not allowed");
    }
    // 检查路径是否包含 ../ 等路径穿越
    if (preg_match('/\.\.\//', $parsed['path'])) {
        throw new Exception("Path traversal detected");
    }
    return true;
}

对 cURL 进行精细化配置

<?php
function secureCurlRequest($url) {
    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 5,
        CURLOPT_CONNECTTIMEOUT => 3,
        CURLOPT_FOLLOWLOCATION => false,  // 手动处理重定向
        CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,  // 限制协议
        CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
        CURLOPT_MAXREDIRS => 0,          // 禁止重定向
        CURLOPT_SSL_VERIFYPEER => true,  // 验证 SSL 证书
        CURLOPT_SSL_VERIFYHOST => 2,
        // 禁止使用本地文件协议(避免 file:// 攻击)
        CURLOPT_FILE => null,
    ]);
    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $finalUrl = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
    // 检查最终请求的 URL 是否合法
    if ($finalUrl !== $url) {
        throw new Exception("URL changed during request");
    }
    curl_close($ch);
    return $response;
}

网络层限制(最底层防御)

在服务器防火墙层面限制出站流量,只允许必要的出站 IP/端口。

# iptables 禁止访问私有 IP 地址
iptables -A OUTPUT -d 10.0.0.0/8 -j DROP
iptables -A OUTPUT -d 172.16.0.0/12 -j DROP
iptables -A OUTPUT -d 192.168.0.0/16 -j DROP
iptables -A OUTPUT -d 127.0.0.0/8 -j DROP
iptables -A OUTPUT -d 169.254.0.0/16 -j DROP
# 只允许访问特定外部服务
iptables -A OUTPUT -d api.example.com -j ACCEPT
iptables -A OUTPUT -j DROP  # 默认拒绝所有其他出站

防御 DNS 重绑定攻击

攻击者可能使用域名在第一次解析时返回正常 IP,第二次返回内部 IP,使用 多次 DNS 解析并比较 IP

<?php
function dnsRebindingCheck($host) {
    $ips1 = dns_get_record($host, DNS_A);
    $ips2 = dns_get_record($host, DNS_A);
    if ($ips1 !== $ips2) {
        throw new Exception("DNS rebinding attack detected");
    }
    // 或者使用 gethostbyname 获取一个 IP,再使用 dns_get_record 获取多个
    $ip1 = gethostbyname($host);
    $records = dns_get_record($host, DNS_A);
    $ip2 = $records[0]['ip'] ?? '';
    if ($ip1 !== $ip2) {
        throw new Exception("Inconsistent DNS resolution");
    }
}

综合防御示例

<?php
class SsrfGuard {
    private $whitelist = [];
    private $blockPrivate = true;
    public function validate($url) {
        $parsed = parse_url($url);
        // 1. 检查 scheme
        if (!in_array($parsed['scheme'], ['http', 'https'])) {
            throw new \Exception("Invalid scheme");
        }
        // 2. 检查白名单
        if (!empty($this->whitelist)) {
            if (!in_array($parsed['host'], $this->whitelist)) {
                throw new \Exception("Host not in whitelist");
            }
        }
        // 3. 阻止私有 IP
        if ($this->blockPrivate) {
            $ip = gethostbyname($parsed['host']);
            if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
                throw new \Exception("Private IP not allowed");
            }
        }
        // 4. 禁止路径穿越
        if (preg_match('/\.\.\//', $parsed['path'] ?? '')) {
            throw new \Exception("Path traversal detected");
        }
        return true;
    }
    public function fetch($url) {
        $this->validate($url);
        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 5,
            CURLOPT_FOLLOWLOCATION => false,
            CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
            CURLOPT_SSL_VERIFYPEER => true,
        ]);
        $result = curl_exec($ch);
        curl_close($ch);
        return $result;
    }
}

总结防御要点

  1. 永远不要信任用户输入的 URL,即使是看起来正常的域名。
  2. 优先使用白名单,如果无法使用白名单,至少使用黑名单过滤私有 IP。
  3. 禁用 cURL 的重定向 或者手动验证每个跳转。
  4. 限制协议 只允许 http/https,禁止 file/ftp/gopher 等协议。
  5. 在系统和网络层面 配置防火墙规则限制出站流量。
  6. SSRF 漏洞常见于:图片抓取、网页截图、URL 预览、API 代理、文件下载等功能。

抱歉,评论功能暂时关闭!