博客 / 詳情

返回

CodeQL對Java項目進行SSRF審計總結

背景

繼之前寫的用CodeQL審計Java項目的SQL注入漏洞,這篇繼續聊另一個高頻的SSRF漏洞。

SSRF(服務器端請求偽造)簡單説就是攻擊者能誘導服務器代替他去訪問內部系統或其他網站。這種漏洞在Java項目裏很常見,因為Java的網絡庫和框架太多,稍不注意就可能給攻擊者留下一條“內網通道”。

和上次一樣,我們還是用CodeQL來做“SSRF探測工具”,高效定位潛在風險。

這裏推薦一個我之前開源的代碼審計項目
https://github.com/78778443/swallow

image

二、CodeQL回顧

2.1 CodeQL使用

操作流程和上次一樣,分三步:

  1. 創建數據庫codeql database create java-ssrf-db --language=java --command="mvn compile"
  2. 執行掃描codeql database analyze java-ssrf-db codeql/java/ql/src/Security/ --format=sarif-latest --output=ssrf-results.sarif
  3. 查看結果:打開生成的 ssrf-results.sarif 文件,重點關注 ruleId 中包含 ssrfhttp-request 的條目。

完成掃描後,我們就可以基於這些結果,開始人工分析了。

2.2 漏洞判斷方法

判斷SSRF漏洞的真偽,和SQL注入的思路一致,主要看三點:

  1. 注入點(Source):數據是否來自不可信的用户輸入?比如URL參數、HTTP頭、請求體等。
  2. 執行點(Sink):被調用的函數是否真的能發起網絡請求?這是判斷的關鍵。
  3. 鏈路過濾:數據從源頭到執行點的過程中,是否經過了有效的校驗或過濾?

三、SSRF漏洞案例實戰

Java中發起HTTP請求的方式很多,所以SSRF的執行點也比較多樣。常見的有:

  • new URL(url).openConnection()
  • HttpClient.execute(request)
  • OkHttpClient.newCall(request).execute()
  • 甚至像 ImageIO.read(url) 這類看似普通的方法,其實也能發起請求。

3.1 常規SSRF漏洞案例

CodeQL報告:在 WebhookController.java 中發現SSRF漏洞,用户輸入參數直接傳入 URL 構造函數並調用 openConnection 方法。

我們來看具體代碼:

// WebhookController.java
public String triggerWebhook(@RequestParam String webhookUrl, @RequestParam String message) {
    try {
        // 用户直接控制整個URL
        URL url = new URL(webhookUrl); // Source: 用户傳入的webhookUrl
        HttpURLConnection conn = (HttpURLConnection) url.openConnection(); // Sink: 發起網絡連接
        conn.setRequestMethod("GET");
        // 設置請求頭,攜帶message參數
        conn.setRequestProperty("X-Message", message);
        // 獲取響應碼
        int responseCode = conn.getResponseCode();
        // 讀取響應內容
        BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
        String inputLine;
        StringBuffer response = new StringBuffer();
        while ((inputLine = in.readLine()) != null) {
            response.append(inputLine);
        }
        in.close();
        return "Webhook triggered! Response: " + response.toString();
    } catch (Exception e) {
        return "Error: " + e.getMessage();
    }
}

審計過程

  1. 源頭分析webhookUrl 是通過 @RequestParam 直接獲取的HTTP請求參數,攻擊者可以隨意構造,比如 http://192.168.1.100:8080/secret(內網系統)或 http://169.254.169.254/latest/meta-data/(雲服務器元數據接口)。源頭完全可控,風險極高。✅ 有效!
  2. 執行點分析:代碼先通過 new URL(webhookUrl) 構造URL對象,再調用 openConnection() 建立連接——這是Java標準庫中發起HTTP/HTTPS請求的核心流程,會實際觸發服務器的網絡請求。後續的 getResponseCode()getInputStream() 會進一步執行請求。這個執行點具備完整的網絡請求能力,是SSRF的典型危險函數。✅ 有效!
  3. 鏈路過濾分析:從 webhookUrl 傳入到發起請求,全程沒有任何校驗邏輯。既沒有檢查域名是否在白名單內,也沒有限制協議類型(比如禁止 file://gopher:// 等),更沒有過濾內網IP。數據從用户輸入到危險操作,完全未經過濾。❌

所以這是一個真漏洞。攻擊者可利用此漏洞探測內網服務、訪問敏感元數據,甚至通過 gopher:// 協議構造惡意命令實施進一步攻擊。修復時必須添加嚴格的URL校驗。

3.2 Spring框架中的SSRF漏洞

很多Java項目會用Spring框架的 RestTemplate 調用外部HTTP服務,這種封裝工具雖然方便,但使用不當也會導致SSRF。

CodeQL報告:在 ApiCallerService.java 中發現SSRF,用户輸入參數參與URL構造並傳入 RestTemplate.getForEntity 方法。

// ApiCallerService.java
@Service
public class ApiCallerService {
    @Autowired
    private RestTemplate restTemplate;

    public String callExternalApi(String userInputUrl) {
        // 用户控制URL前綴部分
        String apiUrl = userInputUrl + "/api/data"; // Source: userInputUrl
        // 發起GET請求並獲取響應
        ResponseEntity<String> response = restTemplate.getForEntity(apiUrl, String.class); // Sink: 執行HTTP請求
        return response.getBody();
    }
}

審計過程

  1. 源頭分析userInputUrl 是外部傳入的參數,攻擊者可自由控制。比如傳入 http://127.0.0.1:8080,最終的 apiUrl 會變成 http://127.0.0.1:8080/api/data,直接指向本地服務;傳入 http://evil.com 則會讓服務器向惡意網站發送請求。源頭完全不可信。✅ 有效!
  2. 執行點分析restTemplate.getForEntity(apiUrl, String.class) 是Spring封裝的HTTP GET請求方法,內部會通過HTTP客户端(默認是JDK的 HttpURLConnection)發起實際網絡請求,具備完整的請求能力,是常見的SSRF危險執行點。✅ 有效!
  3. 鏈路過濾分析:代碼將用户輸入的 userInputUrl 直接拼接固定後綴 /api/data 形成最終URL,中間無任何校驗。即便用户只控制URL的一部分,只要能影響主機名或IP,就足以發起惡意請求。比如傳入 http://192.168.0.5:9000/,就能訪問內網9000端口服務。❌

所以這是一個真漏洞。即使用户僅控制URL的部分內容,只要能構造出可控的完整URL,就存在SSRF風險。修復時需對 userInputUrl 進行嚴格校驗,比如限制協議為 https、域名在白名單內等。


四、常見SSRF漏洞誤報

SSRF的誤報比SQL注入更多,因為“發起網絡請求”是很常見的操作。下面三個案例看起來像漏洞,但實際不是,我們來具體分析。

4.1 URL完全固定

CodeQL報告:在 ConfigLoader.java 中發現SSRF,檢測到 RestTemplate.getForObject 方法調用,參數為動態構造的URL。

// ConfigLoader.java
public void loadConfig() {
    // 從配置文件讀取的可信域名,配置文件由管理員維護
    String configDomain = configProperties.getTrustedConfigDomain(); 
    // 拼接固定路徑,整個URL完全不可變
    String configUrl = "https://" + configDomain + "/static/config/v1.json";
    RestTemplate rt = new RestTemplate();
    // 發起請求獲取配置
    String config = rt.getForObject(configUrl, String.class); // Sink點被CodeQL標記
    // 解析配置並應用...
}

審計過程

  1. 源頭分析configUrl 由固定前綴 https://、管理員預設的 configDomain(如 config.company.com)、固定後綴 /static/config/v1.json 組成。整個URL的所有部分都不受用户控制,攻擊者無法修改。
  2. 執行點分析RestTemplate.getForObject 確實會發起HTTP請求,是SSRF的典型危險執行點,CodeQL的檢測沒錯。
  3. 鏈路過濾分析:數據流從固定配置項和字符串常量拼接成URL,再傳入請求方法,全程無用户輸入參與,是系統內部的固定流程。

結論假漏洞。CodeQL可能因檢測到“字符串拼接+網絡請求”的模式而報警,但未深入分析URL是否來自用户輸入。對於完全固定的URL請求,即使調用危險函數,也不存在SSRF風險。

4.2 URL白名單和解析校驗

CodeQL報告:在 ImageProxyServlet.java 中發現SSRF,用户輸入參數 url 直接傳入 ImageIO.read 方法,該方法可發起網絡請求。

// ImageProxyServlet.java
public void doGet(HttpServletRequest request, HttpServletResponse response) {
    String imageUrl = request.getParameter("url");
    if (imageUrl == null || imageUrl.isEmpty()) {
        response.sendError(400, "Missing url parameter");
        return;
    }
    
    // 第一重校驗:白名單域名檢查
    Set<String> allowedDomains = new HashSet<>(Arrays.asList("cdn.example.com", "img.example.com"));
    String domain;
    try {
        // 解析URL獲取域名(自動處理IP地址、端口等情況)
        URL urlObj = new URL(imageUrl);
        domain = urlObj.getHost();
        // 檢查域名是否在白名單內,同時禁止IP地址(防止繞過域名校驗)
        if (!allowedDomains.contains(domain) || isIpAddress(domain)) {
            throw new SecurityException("Invalid domain");
        }
        // 第二重校驗:限制協議只能是HTTPS
        if (!"https".equals(urlObj.getProtocol())) {
            throw new SecurityException("Only HTTPS is allowed");
        }
        // 第三重校驗:限制端口只能是443
        if (urlObj.getPort() != -1 && urlObj.getPort() != 443) {
            throw new SecurityException("Invalid port");
        }
    } catch (MalformedURLException e) {
        response.sendError(400, "Invalid URL format");
        return;
    } catch (SecurityException e) {
        response.sendError(403, e.getMessage());
        return;
    }
    
    // 通過所有校驗後,才讀取圖片
    try {
        BufferedImage img = ImageIO.read(new URL(imageUrl)); // Sink點
        ImageIO.write(img, "png", response.getOutputStream());
    } catch (Exception e) {
        response.sendError(500, "Error loading image");
    }
}

// 輔助方法:判斷是否為IP地址(包括IPv4和IPv6)
private boolean isIpAddress(String host) {
    try {
        InetAddress.getByName(host);
        return true;
    } catch (UnknownHostException e) {
        return false;
    }
}

審計過程

  1. 源頭分析imageUrl 來自用户請求參數,攻擊者可隨意傳入,源頭不可信。
  2. 執行點分析ImageIO.read(url) 會從URL讀取圖片數據,內部會發起HTTP請求,屬於SSRF的危險執行點,CodeQL的檢測準確。
  3. 鏈路過濾分析:用户輸入的 imageUrl 在到達執行點前,經過了三重嚴格校驗:

    • 白名單域名校驗:只允許 cdn.example.comimg.example.com,且禁止IP地址(防止用 http://192.168.1.100 繞過)。
    • 協議限制:僅允許 https,禁止 httpfilegopher 等危險協議。
    • 端口限制:僅允許443端口,防止訪問內網其他服務。
      這些校驗幾乎堵死了所有攻擊路徑,攻擊者無法構造出通過校驗的惡意URL。

所以這是一個假漏洞。CodeQL的默認規則通常無法識別這種自定義的多步驟校驗,只會看到“用户輸入→網絡請求”的鏈路就報警。但實際上,只要校驗邏輯嚴謹,即使存在用户輸入和網絡請求,也不會有SSRF風險。

4.3 未發起請求

CodeQL報告:在 UrlMonitorService.java 中發現SSRF,用户輸入的URL參數被傳入 log.info 方法,存在潛在風險。

// UrlMonitorService.java
@Service
public class UrlMonitorService {
    private static final Logger log = LoggerFactory.getLogger(UrlMonitorService.class);

    public void recordAccess(String userProvidedUrl) {
        // 解析URL中的域名,僅記錄域名信息
        String domain = "unknown";
        try {
            URL url = new URL(userProvidedUrl);
            domain = url.getHost();
        } catch (MalformedURLException e) {
            // 忽略無效URL格式,記錄原始字符串
        }
        // 僅將域名或原始字符串寫入日誌
        log.info("User accessed domain: {}", domain); // 被CodeQL標記為Sink點
    }
}

審計過程

  1. 源頭分析userProvidedUrl 是用户傳入的參數,攻擊者可控制,源頭不可信。
  2. 執行點分析:CodeQL可能因檢測到“用户輸入的URL字符串被處理”而報警,但 log.info 僅用於將字符串寫入日誌文件(如本地log文件)。它不會解析網絡地址,不會建立TCP連接,更不會發送HTTP請求——完全沒有發起網絡請求的能力,屬於“偽執行點”。
  3. 鏈路過濾分析:用户輸入的URL最多被用於解析域名,最終只有域名或原始字符串被記錄到日誌,全程無任何網絡請求操作。數據流向的是日誌系統,而非網絡請求函數。

所以這是一個假漏洞。因為CodeQL對“URL處理函數”的定義較寬泛,可能將所有接收URL格式字符串的方法納入監測,但忽略了該方法是否真能發起網絡請求。對於僅用於日誌、展示等非網絡操作的場景,即使處理用户提供的URL,也不會構成SSRF風險。


五、SSRF漏洞總結

總結一下用CodeQL審計Java SSRF的要點:

  1. 明確危險執行點:SSRF的核心是能發起網絡請求的函數。要熟悉Java中的各種HTTP客户端(HttpURLConnectionHttpClientRestTemplate等)和隱式發起請求的API(如 ImageIO.read、XML解析器的外部實體加載)。
  2. 跟蹤完整數據流:必須親自跟蹤從用户輸入到危險函數的完整路徑,不能僅依賴CodeQL的報告摘要。
  3. 重視校驗邏輯:SSRF的防禦主要依賴校驗:

    • 優先使用白名單:只允許訪問已知可信的域名,且需解析URL後校驗(防止用IP繞過)。
    • 限制協議和端口:禁止 file://gopher:// 等危險協議,限制端口為80/443等常規端口,禁止內網IP段。
  4. 識別常見誤報來源

    • 源頭不可控:URL是硬編碼或來自可信配置。
    • 中間有強校驗:存在白名單、協議/端口限制、IP過濾等。
    • 執行點無風險:數據僅用於日誌、展示,未發起網絡請求。

作者:湯青松
日期:2025年11月20日
微信:songboy8888

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.