Skip to content

CWE-245: J2EE Bad Practices: Direct Management of Connections

Overview

MITRE describes this weakness as the J2EE standard forbidding the direct management of connections. Read that as a statement about properties rather than about containers: the current Jakarta EE specifications carry no such prohibition, and the Spring Boot pattern below - an executable jar with a framework-configured pool and no container anywhere in it - is a correct fix that the rule as literally worded would reject.

What DriverManager.getConnection() in a request path actually costs is the three things a managed resource would have supplied. It opens a raw, unpooled physical connection per call. It is almost always written with the credentials in source or in a config file, though nothing about the API requires that - the same call reading from the environment is fine, and CWE-798 shows it. And it makes connection lifecycle and cleanup the developer's problem rather than the platform's.

MITRE records a single consequence for this entry - quality degradation - with no security impact at all, so the position taken here goes deliberately further than the catalogue: the security consequence is availability and credential exposure rather than a memory-safety or injection bug, which is why this finding is often deferred. Under load it is a self-inflicted denial of service, and the credentials it forces into the artifact outlive the code that used them.

Relationship to Other CWEs

MITRE relates this page to nothing this corpus covers - not even to CWE-382, despite the shared J2EE Bad Practices prefix, which sits under CWE-705 rather than under this page's parent. The bullets below say how each one is actually reached.

  • CWE-245 (this page) - application code managing its own database connections in a container that offers a managed pool
  • CWE-695 (Use of Low-Level Functionality) - the Base this page sits under, for reaching past a facility the platform provides. No page here
  • CWE-111 (Direct Use of Unsafe JNI) - the other child of CWE-695 this corpus covers, and the closest thing to a sibling: the same shape of defect, reaching past the managed environment, with native code rather than connections as the thing reached for
  • CWE-243 (Creation of chroot Jail Without Changing Working Directory) - further up the same branch, under CWE-573 (Improper Following of Specification by Caller), which is CWE-695's own parent. Another case of using a platform facility in a way its specification does not sanction
  • CWE-382 (J2EE Bad Practices: Use of System.exit()) - the page a reader most often means when they arrive here by name. MITRE records no relationship between them: CWE-382 is under CWE-705 (Incorrect Control Flow Scoping), and the shared prefix is a naming convention rather than a shared parent
  • CWE-404 (Improper Resource Shutdown or Release) - what hand-managed connections usually turn into once one path forgets to close. MITRE records no relationship, but the leak is the practical consequence this page's fix removes

OWASP Classification

A06:2025 - Insecure Design

Risk

Medium: Every request pays full connection setup instead of borrowing from a pool: TCP handshake, TLS negotiation, authentication and session initialisation. On loopback with no TLS that is a couple of milliseconds; across a network it is dominated by round trips, so the number worth having is your own RTT multiplied by them rather than any published figure. Response time degrades under concurrency, and the database's max_connections limit is eventually exhausted, rejecting unrelated applications sharing the same server. Hard-coded credentials travel in the compiled artifact and in version-control history, and a code path that misses close() leaks a connection slot for as long as the process lives. Moving to a pool does not change that: HikariCP's leak detection logs a warning and a stack trace, and leaves the borrowed connection exactly where it is.

Common Vulnerable Patterns

DriverManager in request-handling code

// VULNERABLE - a new physical connection per request, credentials in source
@GetMapping("/api/products/{id}")
public Product getProduct(@PathVariable Long id) throws SQLException {
    Connection conn = DriverManager.getConnection(
        "jdbc:postgresql://db.example.com:5432/store", "appuser", "apppassword");
    try (PreparedStatement stmt = conn.prepareStatement(
            "SELECT * FROM products WHERE id = ?")) {
        stmt.setLong(1, id);
        ResultSet rs = stmt.executeQuery();
        return rs.next() ? mapProduct(rs) : null;
    } finally {
        conn.close();  // closes the physical connection - there is no pool to return to
    }
}

Why this is vulnerable: Under moderate concurrency this both slows every request and consumes the database's connection budget, and the failure hits whatever else shares that database rather than only this endpoint. The credentials are readable in decompiled bytecode and, once committed, remain in history after the code is fixed - rotating them is a separate task from fixing this line.

A connection that is never closed on any path

// VULNERABLE - the connection is never closed, on any path
public User findUser(String username) throws SQLException {
    Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);

    PreparedStatement stmt = conn.prepareStatement(
        "SELECT * FROM users WHERE username = ?");
    stmt.setString(1, username);
    ResultSet rs = stmt.executeQuery();

    return rs.next() ? mapUser(rs) : null;
    // conn is never closed - one slot lost per call, and per thrown exception
}

Why this is vulnerable: Nothing closes the connection on any path - not the successful return, and not an exception between getConnection() and the end of the method. The database holds each abandoned connection until the client process exits or the socket closes, and a server-side idle timeout will not rescue you - PostgreSQL's idle_session_timeout is zero, meaning disabled, by default. Every call costs a slot until max_connections is reached.

Secure Patterns

Container-managed DataSource via JNDI

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;

// SECURE - pooled, container-configured, credentials outside the application
public class UserDAO {
    private final DataSource dataSource;

    public UserDAO() throws NamingException {
        InitialContext ctx = new InitialContext();
        this.dataSource = (DataSource) ctx.lookup("java:comp/env/jdbc/MyDB");
    }

    public User findUser(String username) throws SQLException {
        try (Connection conn = dataSource.getConnection();
             PreparedStatement stmt = conn.prepareStatement(
                 "SELECT * FROM users WHERE username = ?")) {
            stmt.setString(1, username);
            try (ResultSet rs = stmt.executeQuery()) {
                return rs.next() ? mapUser(rs) : null;
            }
        }  // connection returns to the pool here, including on exception
    }
}

Why this works: The container owns the pool, the connection limit and the credentials, configured once in context.xml or the server's own configuration rather than in the deployable artifact. getConnection() borrows from that pool and close() returns the connection rather than tearing it down, so the cost is paid at startup instead of per request. try-with-resources closes the ResultSet, PreparedStatement and Connection in reverse order on every exit path, which is the part a hand-written finally block routinely gets wrong.

Spring Boot, where the pool is configured rather than created

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

// SECURE - Spring Boot auto-configures a HikariCP pool behind this
@Repository
public class ProductRepository {
    private final JdbcTemplate jdbcTemplate;

    public ProductRepository(JdbcTemplate jdbcTemplate) {   // constructor injection
        this.jdbcTemplate = jdbcTemplate;
    }

    public Product findById(Long id) {
        return jdbcTemplate.queryForObject(
            "SELECT id, name FROM products WHERE id = ?",
            (rs, rowNum) -> new Product(rs.getLong("id"), rs.getString("name")),
            id);
    }
}
application.yml
spring:
  datasource:
    url: ${DB_URL}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
    hikari:
      maximum-pool-size: 20
      leak-detection-threshold: 60000   # warn when a connection is held over 60s

Why this works: No application code obtains or releases a connection, so there is no code path that can forget to. The credentials come from the environment, so the artifact carries none. leak-detection-threshold is worth setting explicitly: it turns a slow leak into a log warning during testing rather than a pool exhaustion in production months later. The row-mapper overload used here is the current one - queryForObject(String, Object[], RowMapper) has been deprecated since Spring 5.3.

Outside Spring, @Resource(lookup = "java:app/jdbc/MyDB") injection in Jakarta EE, or a HikariCP DataSource built once at startup, gives the same properties.

Considerations

  • DriverManager is correct outside a container. A CLI tool, a migration runner, a test fixture or a one-shot batch job has no container to ask, and a single short-lived connection is the right shape for it. The weakness is specifically application code inside a managed environment doing its own resource management - a finding on a standalone main() is a false positive and should be recorded as one.
  • Fixing the leak is not fixing the finding. Wrapping the existing DriverManager call in try-with-resources removes the leak and leaves the per-request connection cost and the embedded credentials exactly as they were. If the work is being staged, say which half shipped.
  • Pool size is a database-side decision, not an application one. The ceiling that matters is the server's max_connections divided across every application and instance connecting to it. A pool of 20 per instance across 10 instances is 200 connections, and a default max_connections of 100 fails well before the application thinks it is under load.
  • Credentials already committed stay committed. Moving them to the environment stops new exposure; it does not remove them from history or from artifacts already published. Rotation is the remediation for what has already leaked, and it is a separate task with a separate owner.
  • Prefer framework-managed transactions to manual control on a pooled connection. @Transactional or JTA restores the connection's state on every path, including the ones an early return or a thrown exception takes. Manual setAutoCommit(false) with hand-written commit()/rollback() is not unsafe with a pool that cleans up after a borrower - HikariCP rolls back an uncommitted transaction and resets the connection state it tracks when the connection is returned - but it moves the burden onto every path through your own code being correct, and that guarantee is the pool's, not the JDBC API's.

Testing

The scanner can confirm DriverManager is gone. Whether connections are pooled and returned is only observable under load.

  • Load-test the endpoint and assert on the pool's own metrics, not the database's session count. Once the run drains, active connections return to zero, idle returns to the pool size, and a fresh borrow still succeeds - HikariCP exposes getActiveConnections() and getIdleConnections() on HikariPoolMXBean, or the hikaricp_connections_active and _idle meters. Sampling the database (SELECT count(*) FROM pg_stat_activity, or SHOW STATUS LIKE 'Threads_connected') is worth doing alongside, because it shows the pool is capping sessions at all - but on its own it cannot tell a healthy pool from one whose twenty connections have every one leaked to a borrower, since both report twenty sessions.
  • Drive the error paths under the same load - a request that throws inside the query - and assert the pool's active count still returns to zero afterwards. This is what catches a fix applied to the happy path only.
  • Enable leak-detection-threshold in the test environment and assert no warnings are logged during a full suite run. A clean run is evidence for the paths the suite exercises, and no more.
  • Assert the built artifact contains no credentials: grep the packaged JAR/WAR and the rendered configuration, not just the source tree.
  • Assert the application fails to start when the datasource environment variables are absent, though the URL and the credentials behave differently. With url: ${DB_URL} and nothing in the environment, Spring binds the unresolved placeholder as literal text and the context then refuses to refresh, because the driver-class lookup rejects a URL that does not begin jdbc - so that one fails loudly on its own. With a valid URL but DB_USERNAME unresolved, the application starts cleanly and the literal only bites at the first connection. Assert the startup failure for the URL, and add an explicit check for the credentials rather than assuming they behave the same way. A fallback default of the form ${DB_URL:jdbc:postgresql://localhost:5432/myapp} removes even the URL's protection, which is why the block above carries none.

Additional Resources