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);
}
}
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
DriverManageris 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 standalonemain()is a false positive and should be recorded as one.- Fixing the leak is not fixing the finding. Wrapping the existing
DriverManagercall 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_connectionsdivided across every application and instance connecting to it. A pool of 20 per instance across 10 instances is 200 connections, and a defaultmax_connectionsof 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.
@Transactionalor JTA restores the connection's state on every path, including the ones an early return or a thrown exception takes. ManualsetAutoCommit(false)with hand-writtencommit()/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()andgetIdleConnections()onHikariPoolMXBean, or thehikaricp_connections_activeand_idlemeters. Sampling the database (SELECT count(*) FROM pg_stat_activity, orSHOW 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-thresholdin 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 beginjdbc- so that one fails loudly on its own. With a valid URL butDB_USERNAMEunresolved, 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
- CWE-245: J2EE Bad Practices: Direct Management of Connections
- Jakarta EE Platform specifications - the version index; DataSource resource definitions are a section inside the platform specification document itself
- HikariCP configuration
- OWASP Database Security Cheat Sheet
- OWASP Top 10 2025 A06: Insecure Design