content: // Copyright (c) 2025 Alibaba Group and its affiliates
  
  // Licensed under the Apache License, Version 2.0 (the "License");
  // you may not use this file except in compliance with the License.
  // You may obtain a copy of the License at
  
  //     http://www.apache.org/licenses/LICENSE-2.0
  
  // Unless required by applicable law or agreed to in writing, software
  
  // distributed under the License is distributed on an "AS IS" BASIS,
  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  // See the License for the specific language governing permissions and
  // limitations under the License.
  
  package com.example.test;
  
  import com.example.service.ImageService;
  import com.sun.net.httpserver.HttpExchange;
  import com.sun.net.httpserver.HttpHandler;
  import com.sun.net.httpserver.HttpServer;
  import org.junit.After;
  import org.junit.Assert;
  import org.junit.Before;
  import org.junit.Test;
  
  import java.io.IOException;
  import java.io.OutputStream;
  import java.net.InetSocketAddress;
  import java.net.ServerSocket;
  import java.util.UUID;
  import java.util.concurrent.atomic.AtomicBoolean;
  
  /**
   * 安全性测试，用于检测ImageService中的SSRF（服务器端请求伪造）漏洞。
   * 测试旨在在存在漏洞时失败，在修复漏洞后通过。
   */
  public class SecurityTest {
  
      private ImageService imageService;
      private HttpServer localServer;
      private int localServerPort;
      private final String ATTACK_ENDPOINT_PATH = "/" + UUID.randomUUID().toString();
      private final AtomicBoolean requestReceived = new AtomicBoolean(false);
  
      @Before
      public void setUp() throws IOException {
          imageService = new ImageService();
          int maxRetries = 10;
          for (int i = 0; i < maxRetries; i++) {
              try {
                  try (ServerSocket s = new ServerSocket(0)) {
                      localServerPort = s.getLocalPort();
                  }
  
                  localServer = HttpServer.create(new InetSocketAddress("0.0.0.0", localServerPort), 0);
                  localServer.createContext(ATTACK_ENDPOINT_PATH, new LocalServerHandler());
                  localServer.setExecutor(null);
                  localServer.start();
  
                  System.out.println("Local test server started successfully on port: " + localServerPort);
                  return;
  
              } catch (IOException e) {
                  System.err.println("Port " + localServerPort + " was occupied, retrying... (" + (i + 1) + "/" + maxRetries + ")");
                  if (i == maxRetries - 1) {
                      throw new IOException("Failed to start local test server after " + maxRetries + " attempts.", e);
                  }
              }
          }
      }
  
      @After
      public void tearDown() {
          if (localServer != null) {
              localServer.stop(0);
          }
      }
  
      /**
       * 测试通过提供一个指向本地资源的直接 IP 地址来模拟 SSRF 攻击。
       */
      @Test
      public void testSsrAttackWithDirectIpUrl() {
          String maliciousUrl = String.format("http://127.0.0.1:%d%s", localServerPort, ATTACK_ENDPOINT_PATH);
          assertSsrfaIsBlocked(maliciousUrl, "Direct IP access");
      }
  
      /**
       * 测试通过 DNS 解析服务绕过基于主机名的 SSRF 过滤器。
       */
      @Test
      public void testSsrAttackViaDns() {
          String maliciousUrl = String.format("http://127.0.0.1.nip.io:%d%s", localServerPort, ATTACK_ENDPOINT_PATH);
          assertSsrfaIsBlocked(maliciousUrl, "Magic DNS service (nip.io)");
      }
  
      /**
       * 测试通过 HTTP 302 重定向到内部网络的 SSRF 攻击。
       */
      @Test
      public void testRedirectToInternalNetworkAccess() {
          String url = String.format("http://localhost:%d%s", localServerPort, ATTACK_ENDPOINT_PATH);
          // 此URL会302重定向到查询参数中的地址
          String redirectUrlFormat = "http://drsupport.oss-cn-beijing.aliyuncs.com/heimdall_ssrf_redirect.png?" + url;
          assertSsrfaIsBlocked(redirectUrlFormat, "302 redirect to internal network");
      }
  
      /**
       * 测试使用各种形式的 localhost 和环回地址（如 'localhost', '0'）的 SSRF 攻击。
       */
      @Test
      public void testSsrAttackWithLoopbackVariations() {
          assertSsrfaIsBlocked(String.format("http://localhost:%d%s", localServerPort, ATTACK_ENDPOINT_PATH),
                  "Hostname 'localhost'");
  
          assertSsrfaIsBlocked(String.format("http://0:%d%s", localServerPort, ATTACK_ENDPOINT_PATH),
                  "Special address '0'");
  
          assertSsrfaIsBlocked(String.format("http://0.0.0.0:%d%s", localServerPort, ATTACK_ENDPOINT_PATH),
                  "Special address '0.0.0.0'");
      }
  
      /**
       * 测试使用 IP 地址编码（十进制、八进制、十六进制）进行混淆的 SSRF 攻击。
       */
      @Test
      public void testSsrAttackWithEncodedIpAddresses() {
          assertSsrfaIsBlocked(String.format("http://2130706433:%d%s", localServerPort, ATTACK_ENDPOINT_PATH),
                  "Decimal-encoded IP");
  
          assertSsrfaIsBlocked(String.format("http://0177.0.0.1:%d%s", localServerPort, ATTACK_ENDPOINT_PATH),
                  "Octal-encoded IP");
  
          assertSsrfaIsBlocked(String.format("http://0x7f.0.0.1:%d%s", localServerPort, ATTACK_ENDPOINT_PATH),
                  "Hex-encoded IP");
      }
  
      /**
       * 测试使用 IPv6 环回地址（包括标准和 IPv4 映射格式）的 SSRF 攻击。
       */
      @Test
      public void testSsrAttackWithIPv6LoopbackAddresses() {
          assertSsrfaIsBlocked(String.format("http://[::1]:%d%s", localServerPort, ATTACK_ENDPOINT_PATH),
                  "IPv6 loopback '[::1]'");
  
          assertSsrfaIsBlocked(String.format("http://[::ffff:127.0.0.1]:%d%s", localServerPort, ATTACK_ENDPOINT_PATH),
                  "IPv4-mapped IPv6 loopback");
  
          assertSsrfaIsBlocked(String.format("http://[::ffff:7f00:1]:%d%s", localServerPort, ATTACK_ENDPOINT_PATH),
                  "IPv4-mapped IPv6 loopback (Hex)");
      }
  
      /**
       * 测试利用 URL 认证语法（'safedomain@127.0.0.1'）混淆主机的 SSRF 攻击。
       */
      @Test
      public void testSsrAttackWithUrlAuthenticationSyntax() {
          String maliciousUrl = String.format("http://safedomain.com@127.0.0.1:%d%s", localServerPort, ATTACK_ENDPOINT_PATH);
          assertSsrfaIsBlocked(maliciousUrl, "URL authentication syntax with '@'");
      }
  
      /**
       * 测试利用同形异义字符（视觉上相似的 Unicode 字符）混淆 IP 地址的 SSRF 攻击。
       */
      @Test
      public void testSsrAttackWithHomoglyphObfuscation() {
          String urlWithCircledDigit = String.format("http://1②7.0.0.1:%d%s", localServerPort, ATTACK_ENDPOINT_PATH);
          assertSsrfaIsBlocked(urlWithCircledDigit, "Unicode circled digit bypass (1②7.0.0.1)");
  
          String urlWithFullWidthDot = String.format("http://127.0.0。1:%d%s", localServerPort, ATTACK_ENDPOINT_PATH);
          assertSsrfaIsBlocked(urlWithFullWidthDot, "Full-width dot bypass (127.0.0。1)");
      }
  
  
      /**
       * 辅助方法，用于断言SSRF攻击被阻止。
       * 它通过检查本地服务器是否收到了请求来判断攻击是否成功。
       * @param maliciousUrl 恶意构造的URL
       * @param attackDescription 攻击描述
       */
      private void assertSsrfaIsBlocked(String maliciousUrl, String attackDescription) {
          // 在每次调用前重置标志位
          requestReceived.set(false);
  
          try {
              // 调用可能存在漏洞的服务方法
              imageService.processImageFromUrl(maliciousUrl);
          } catch (Exception e) {
              // 忽略所有异常，因为我们的判断依据是请求是否到达服务器，
              // 而不是方法是否抛出异常。一个被阻止的请求很可能就会抛出异常。
          }
  
          // 断言攻击不成功。
          // 如果requestReceived为true，说明Mock服务器收到了请求，存在SSRF漏洞，测试失败。
          Assert.assertFalse(
                  "SSRF VULNERABILITY DETECTED (" + attackDescription + "): The service made an unauthorized network request to " + maliciousUrl,
                  requestReceived.get()
          );
      }
  
  
      /**
       * 用于本地服务器的处理器。
       * 如果这个处理器被调用，它会设置 requestReceived 标志位为 true，表明SSRF攻击成功。
       */
      private class LocalServerHandler implements HttpHandler {
          @Override
          public void handle(HttpExchange exchange) throws IOException {
              requestReceived.set(true);
              String response = "SSRF attack successful";
              exchange.sendResponseHeaders(200, response.length());
              try (OutputStream os = exchange.getResponseBody()) {
                  os.write(response.getBytes());
              }
          }
      }
  }
  