Skip to content

SSRF via JDBC Connection in SpringBlade blade-report /ureport/datasource/testConnection Endpoint #36

Description

@Arron-bit

Affected Versions

  • Product: SpringBlade (https://github.com/chillzhuang/SpringBlade)
  • Affected Component: blade-report module (UReport2 integration)
  • Affected Versions: ≤ 4.8.0 (latest version as of disclosure)
  • Affected Endpoint: POST /ureport/datasource/testConnection

Impact

The /ureport/datasource/testConnection endpoint in the blade-report service accepts fully user-controlled JDBC connection parameters (driver, url, username, password). The server directly calls Class.forName(driver) and DriverManager.getConnection(url, username, password) without any validation or restriction on the supplied values.

This leads to three escalating attack scenarios:

  1. Server-Side Request Forgery (SSRF): An attacker can force the server to initiate TCP connections to arbitrary internal or external hosts and ports via JDBC URLs, enabling internal network reconnaissance and service discovery.
  2. Arbitrary File Read (mysql-connector-j ≥ 8.2.0, the project default): By pointing the JDBC URL to an attacker-controlled fake MySQL server with allowLoadLocalInfile=true, the attacker can exploit the MySQL protocol's LOAD DATA LOCAL INFILE mechanism to exfiltrate arbitrary files from the blade-report server (e.g., /etc/passwd, application configuration files, private keys).
  3. Remote Code Execution (mysql-connector-j < 8.2.0): On older connector versions where autoDeserialize is still available, an attacker can trigger Java deserialization of malicious objects returned by a fake MySQL server, leading to arbitrary code execution on the blade-report server.

Steps to Reproduce

  1. Send the following request to the blade-report service (default port 8108).
Image Image
  1. The server attempts a real TCP connection to 127.0.0.1:13306. The response reveals whether the port is open.
Image
  1. If the specified address is not an internal network address but an attacker-controlled IP, and the JDBC URL parameter allowLoadLocalInfile=true is set, arbitrary file read on the server can be achieved. Use this tool to test.

https://github.com/4ra1n/mysql-fake-server

Root Cause Analysis

The vulnerability originates in UReport2's DatasourceServletAction class, which handles datasource-related operations for the report designer. The testConnection method directly uses user-supplied parameters to establish a JDBC connection without any validation:

// Pseudocode reconstructed from UReport2 DatasourceServletAction
public void testConnection(HttpServletRequest req, HttpServletResponse resp) {
    String driver   = req.getParameter("driver");
    String url      = req.getParameter("url");
    String username = req.getParameter("username");
    String password = req.getParameter("password");

    Connection conn = null;
    Map<String, Object> result = new HashMap<>();
    try {
        // VULNERABILITY 1: Arbitrary class loading
        // Attacker controls the class name passed to Class.forName()
        Class.forName(driver);

        // VULNERABILITY 2: SSRF + Arbitrary JDBC connection
        // Attacker controls the full JDBC URL, username, and password
        // No whitelist, no URL parsing, no parameter filtering
        conn = DriverManager.getConnection(url, username, password);

        result.put("result", true);
    } catch (Exception e) {
        // VULNERABILITY 3: Verbose error disclosure
        // Full exception message returned to attacker aids exploitation
        result.put("result", false);
        result.put("error", e.toString());
    } finally {
        if (conn != null) conn.close();
    }
    // Write result as JSON response
    writeJson(resp, result);
}

Three flaws compound to create the vulnerability:

  1. No input validation on driver: Class.forName(driver) executes with any class name. This loads the class and runs its static initializer. It also enables the attacker to probe which classes exist on the classpath.
  2. No input validation on url: DriverManager.getConnection(url, ...) accepts any JDBC URL. The attacker controls the destination host/port (enabling SSRF) and all connection parameters (enabling allowLoadLocalInfile, autoDeserialize, etc.).
  3. No network-level restriction: The server makes outbound connections to any address specified in the URL, including internal RFC1918 addresses, localhost, and external attacker-controlled servers.

Additionally, SpringBlade's UReportAuthFilter only requires a valid session token (report-viewer permission level), providing insufficient access control for such a dangerous operation.

Remediation

Immediate Mitigation

  1. Disable the endpoint: If the datasource test functionality is not required in production, block access to /ureport/datasource/testConnection at the gateway or web filter level.

Code-Level Fixes

  1. Whitelist allowed JDBC drivers: Replace Class.forName(driver) with a check against a predefined set of allowed driver class names:

    Set<String> ALLOWED_DRIVERS = Set.of("com.mysql.cj.jdbc.Driver", "org.postgresql.Driver");
    if (!ALLOWED_DRIVERS.contains(driver)) {
        throw new SecurityException("Driver not allowed: " + driver);
    }
  2. Sanitize JDBC URL parameters: Parse the JDBC URL and strip dangerous parameters before connecting:

    List<String> BLOCKED_PARAMS = List.of(
        "autoDeserialize", "allowLoadLocalInfile", "allowUrlInLocalInfile",
        "queryInterceptors", "statementInterceptors", "detectCustomCollations"
    );
  3. Restrict connection targets: Validate that the host in the JDBC URL is not a private/loopback address (prevent SSRF to internal network):

    InetAddress addr = InetAddress.getByName(extractedHost);
    if (addr.isLoopbackAddress() || addr.isSiteLocalAddress() || addr.isLinkLocalAddress()) {
        throw new SecurityException("Connection to internal addresses is not allowed");
    }
  4. Disable LOAD DATA LOCAL at the driver level by setting allowLoadLocalInfile=false as a system-wide default, regardless of user-supplied URL parameters.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions