前言

Github:https://github.com/HealerJean

博客:http://blog.healerjean.com

一、SSRF 是什么

SSRFServer-Side Request Forgery)是一种服务器端请求伪造的攻击方式。攻击者通过诱导服务器发起对外部或内部目标的请求,从而绕过网络隔离机制,访问原本无法从外部访问的资源或服务。

1、核心原理:

  • Web 应用程序提供从指定 URL 获取资源的功能(如图片加载、文件下载、API 调用等)时,若未对用户提供的 URL 做严格的校验与限制,攻击者可以构造恶意 URL,使服务器向任意地址发起请求。
  • SSRF 通常被用于:
    • 访问内网资源(如 127.0.0.1192.168.x.x
    • 探测或攻击内部服务(如 RedisMySQLSSRF 反弹 Shell)
    • 利用伪协议(如 file://dict://gopher://)读取本地文件或执行命令
    • 绕过防火墙访问外网限制服务

二、SSRF 的危害

攻击目标 潜在风险
内网服务 Redis 未授权访问、SSRF 反弹 Shell
本地文件 利用 file:// 读取敏感文件
外部服务 利用服务器身份发起攻击
云环境 获取云平台元数据(如 AWS Metadata API)

三、SSRF 的防御措施

1、输入验证和过滤

  • 白名单机制:只允许访问特定域名或 IP 地址。
  • 协议限制:仅允许 httphttps 等安全协议。
  • 禁止伪协议:如 file://dict://gopher:// 等。

2、禁止访问内网地址

  • IP 地址黑名单:拒绝 127.0.0.1192.168.0.0/1610.0.0.0/8172.16.0.0/12 等私有 IP。
  • DNS 解析控制:防止 localhostmetadata.google.internal 等解析为内网地址。

3、端口限制

  • 限制访问端口为常见的 Web 端口,如 80443
  • 防止访问 222325538080 等非 Web 端口。

4、使用安全组件

  • 使用安全的 HTTP 客户端库,如 Apache HttpClientOkHttp
  • 使用 DNS 解析时,避免使用系统默认解析器,防止DNS Rebinding 攻击。

5、强制访问控制

  • 对请求目标进行身份认证和权限控制。
  • 使用代理服务器或中间服务来隔离请求。

6、日志和监控

  • 记录所有 SSRF 请求行为,便于审计和追踪。
  • 设置异常请求的告警机制。

四、优化建议

输入验证和过滤:对从用户输入中获取的URL进行严格的验证和过滤,确保只接受合法的URL。使用白名单过滤机制,限制URL只能访问特定的域名或IP地址,从而防止攻击者构造恶意请求。

限制协议和端口:限制服务器端应用程序只能发起特定协议(如HTTP和HTTPS)和特定端口范围内的请求。同样,使用白名单机制,只允许特定的协议和端口,以减少潜在的攻击面。

内网访问限制:确保服务器端应用程序只能发起外部网络的请求,禁止访问内部网络。利用网络隔离技术,将服务器部署在DMZ(Demilitarized Zone)区域,只允许与外部网络通信,以防止SSRF攻击利用内部网络资源。

更新相关组件和框架:及时更新服务器端应用程序使用的相关组件和框架,以修复已知的SSRF漏洞。保持系统和应用程序的最新版本,可以减少被利用的风险。

强制访问控制:使用身份验证和授权机制,限制用户访问特定的功能和资源。这有助于防止未经授权的请求被发送,从而减少SSRF攻击的可能性。

package com.healerjean.proj.utils.http;

import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Sets;
import lombok.extern.slf4j.Slf4j;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.List;
import java.util.Set;

/**
 * HTTP 工具类:包含安全 URL 校验(防 SSRF)和响应写入功能
 *
 * @author zhangyujin
 * @date 2025/3/17
 */
@Slf4j
public class HttpUtils {

    /**
     * 默认允许的协议(用于 SSRF 防护)
     */
    private static final Set<String> ALLOWED_PROTOCOLS = Sets.newHashSet("http", "https");

    /**
     * 默认允许的端口(用于 SSRF 防护)
     */
    private static final Set<Integer> ALLOWED_PORTS = Sets.newHashSet(80, 443);
  
    /**
     * 全面校验 URL 安全性(用于防止 SSRF 攻击)
     * <p>
     * 校验项包括:
     * 1. 协议是否为 http/https
     * 2. 域名是否在白名单内
     * 3. 是否尝试访问私有 IP(回环、内网等)
     * 4. 端口是否为 80/443
     *
     * @param url            待校验的 URL 字符串
     * @param allowedDomains 允许的域名白名单(支持 .domain.com 通配)
     * @return true 表示安全,false 表示存在风险
     */
    public static boolean sslfUrl(String url, List<String> allowedDomains) {
        if (url == null || url.trim().isEmpty()) {
            log.warn("sslfUrl - 输入 URL 为空");
            return false;
        }

        URI uri;
        try {
            // 使用 URI 而非 URL,避免意外 DNS 查询或连接
            uri = new URI(url);
        } catch (URISyntaxException e) {
            log.warn("sslfUrl - URL 格式非法: {}", url, e);
            return false;
        }

        // 1. 协议检查
        String scheme = uri.getScheme();
        if (scheme == null || !ALLOWED_PROTOCOLS.contains(scheme.toLowerCase())) {
            log.warn("sslfUrl - 协议不合法: {}", url);
            return false;
        }

        // 2. Host 检查
        String host = uri.getHost();
        if (host == null || host.isEmpty()) {
            log.warn("sslfUrl - URL 缺少 host: {}", url);
            return false;
        }
        host = host.toLowerCase();

        if (!isAllowedDomain(host, allowedDomains)) {
            log.warn("sslfUrl - 域名不在白名单内: {}", url);
            return false;
        }

        // 3. 私有地址检查(防止 SSRF)
        try {
            InetAddress address = InetAddress.getByName(host);
            if (address.isAnyLocalAddress() ||
                    address.isLoopbackAddress() ||
                    address.isLinkLocalAddress() ||
                    address.isSiteLocalAddress()) {
                log.warn("sslfUrl - 禁止访问私有或回环地址: {}", url);
                return false;
            }
        } catch (UnknownHostException e) {
            log.warn("sslfUrl - DNS 解析失败: {}", url, e);
            return false;
        }

        // 4. 端口检查
        int port = uri.getPort();
        if (port == -1) {
            // 默认端口:https -> 443, http -> 80
            port = "https".equalsIgnoreCase(scheme) ? 443 : 80;
        }
        if (!ALLOWED_PORTS.contains(port)) {
            log.warn("sslfUrl - 端口不合法: {} (port={})", url, port);
            return false;
        }

        return true;
    }

    /**
     * 判断 host 是否匹配域名白名单规则
     *
     * @param host           目标主机名(已转为小写)
     * @param allowedDomains 白名单列表(如 [".bail.com", "api.trusted.com"])
     * @return 是否匹配
     */
    private static boolean isAllowedDomain(String host, List<String> allowedDomains) {
        for (String pattern : allowedDomains) {
            if (pattern == null || pattern.trim().isEmpty()) {
                continue;
            }
            String cleanPattern = pattern.trim().toLowerCase();

            if (cleanPattern.startsWith(".")) {
                // 通配模式:.example.com
                String domain = cleanPattern.substring(1);
                if (domain.isEmpty()) {
                    continue; // 跳过无效模式如 "."
                }
                // 匹配主域(example.com)或子域(xxx.example.com)
                if (host.equals(domain) || host.endsWith("." + domain)) {
                    return true;
                }
            } else {
                // 精确匹配模式:example.com
                if (host.equals(cleanPattern)) {
                    return true;
                }
            }
        }
        return false;
    }

  
}

ContactAuthor