CWE-245: J2EE Bad Practices: Direct Management of Connections
Overview
Direct management of database connections in J2EE applications bypasses container-managed connection pooling, causing resource exhaustion, connection leaks, hard-coded credentials, inconsistent transaction handling, and performance degradation by creating expensive connections per request.
Risk
Medium: Direct connection management causes connection leaks (DoS), resource exhaustion, hard-coded credentials in code, bypassed connection pooling (performance hit), inconsistent transaction boundaries, and difficulty managing connection limits across multiple servers.
Remediation Steps
Core principle: Use container-managed DataSource with connection pooling; never create connections directly with DriverManager.
Locate Direct Connection Management
When reviewing security scan results:
- Find DriverManager usage: Look for
DriverManager.getConnection()in application code - Check hard-coded credentials: Search for JDBC URLs with passwords
- Identify missing connection closing: Look for connections not in try-with-resources
- Review connection creation patterns: Find new connections per request
- Check for bypassed pooling: Identify code that doesn't use DataSource
Vulnerable patterns:
// Direct connection creation
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost/db",
"root",
"password123" // Hard-coded!
);
// No connection pooling
// Not closed properly
Use Container-Managed Connections (Primary Defense)
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
// VULNERABLE - direct connection management
public class UserDAOBad {
public User findUser(String username) throws SQLException {
// BAD: DriverManager with hard-coded credentials
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/mydb",
"dbuser",
"dbpassword123" // Hard-coded password!
);
// BAD: No connection pooling - creates new connection every call
// BAD: Connection not properly closed
PreparedStatement stmt = conn.prepareStatement(
"SELECT * FROM users WHERE username = ?"
);
stmt.setString(1, username);
ResultSet rs = stmt.executeQuery();
// Process results...
// BUG: Connection never closed - RESOURCE LEAK!
return user;
}
}
// SECURE - container-managed DataSource
public class UserDAOSafe {
private DataSource dataSource;
// Get DataSource from JNDI (container-managed)
public UserDAOSafe() {
try {
InitialContext ctx = new InitialContext();
this.dataSource = (DataSource) ctx.lookup("java:comp/env/jdbc/MyDB");
} catch (NamingException e) {
throw new RuntimeException("Failed to lookup DataSource", e);
}
}
public User findUser(String username) throws SQLException {
// Use try-with-resources to ensure connection is closed
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(
"SELECT * FROM users WHERE username = ?")) {
stmt.setString(1, username);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return mapUser(rs);
}
return null;
}
}
// Connection automatically returned to pool
}
}
// Configuration in web.xml
/*
<resource-ref>
<description>MySQL DataSource</description>
<res-ref-name>jdbc/MyDB</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
</resource-ref>
*/
// Configuration in context.xml (Tomcat)
/*
<Context>
<Resource name="jdbc/MyDB"
auth="Container"
type="javax.sql.DataSource"
maxTotal="100"
maxIdle="30"
maxWaitMillis="10000"
username="dbuser"
password="${DB_PASSWORD}" // From environment
driverClassName="com.mysql.cj.jdbc.Driver"
url="jdbc:mysql://localhost:3306/mydb"/>
</Context>
*/
Why this works: Container-managed DataSource provides connection pooling, credential management via JNDI, proper resource cleanup, and centralized configuration. No hard-coded credentials, efficient connection reuse.
Use Connection Pooling with Spring/Jakarta EE
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import javax.sql.DataSource;
// Spring Boot with HikariCP (auto-configured)
@Repository
public class UserRepository {
@Autowired
private JdbcTemplate jdbcTemplate; // Uses connection pool
public User findByUsername(String username) {
return jdbcTemplate.queryForObject(
"SELECT * FROM users WHERE username = ?",
new Object[]{username},
(rs, rowNum) -> new User(
rs.getLong("id"),
rs.getString("username"),
rs.getString("email")
)
);
}
// JdbcTemplate handles connection management automatically
}
// application.properties
/*
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=dbuser
spring.datasource.password=${DB_PASSWORD}
# HikariCP settings (Spring Boot default)
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.idle-timeout=600000
spring.datasource.hikari.max-lifetime=1800000
*/
// Jakarta EE with CDI
import javax.annotation.Resource;
import javax.enterprise.context.ApplicationScoped;
import javax.sql.DataSource;
@ApplicationScoped
public class UserService {
@Resource(lookup = "java:app/jdbc/MyDB")
private DataSource dataSource;
public User getUser(Long id) throws SQLException {
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(
"SELECT * FROM users WHERE id = ?")) {
stmt.setLong(1, id);
try (ResultSet rs = stmt.executeQuery()) {
return rs.next() ? mapUser(rs) : null;
}
}
}
}
Configure Connection Pooling Properly
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
// Manual HikariCP configuration (if not using Spring Boot)
public class DataSourceConfig {
public static DataSource createDataSource() {
HikariConfig config = new HikariConfig();
// Connection settings
config.setJdbcUrl(System.getenv("DB_URL"));
config.setUsername(System.getenv("DB_USER"));
config.setPassword(System.getenv("DB_PASSWORD"));
// Pool settings
config.setMaximumPoolSize(20); // Max connections
config.setMinimumIdle(5); // Min idle connections
config.setConnectionTimeout(30000); // 30 seconds
config.setIdleTimeout(600000); // 10 minutes
config.setMaxLifetime(1800000); // 30 minutes
// Connection validation
config.setConnectionTestQuery("SELECT 1");
config.setValidationTimeout(5000);
// Performance
config.setAutoCommit(true);
config.setReadOnly(false);
// Leak detection (development)
config.setLeakDetectionThreshold(60000); // 1 minute
return new HikariDataSource(config);
}
}
// Apache DBCP2 configuration
import org.apache.commons.dbcp2.BasicDataSource;
public class DBCPDataSourceConfig {
public static DataSource createDataSource() {
BasicDataSource ds = new BasicDataSource();
ds.setUrl(System.getenv("DB_URL"));
ds.setUsername(System.getenv("DB_USER"));
ds.setPassword(System.getenv("DB_PASSWORD"));
ds.setInitialSize(5);
ds.setMaxTotal(20);
ds.setMaxIdle(10);
ds.setMinIdle(5);
ds.setMaxWaitMillis(30000);
ds.setTestOnBorrow(true);
ds.setValidationQuery("SELECT 1");
return ds;
}
}
Ensure Proper Resource Management
import java.sql.*;
import javax.sql.DataSource;
public class ProperResourceManagement {
private final DataSource dataSource;
public ProperResourceManagement(DataSource dataSource) {
this.dataSource = dataSource;
}
// VULNERABLE - resources not properly closed
public void updateUserBad(Long userId, String email) throws SQLException {
Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(
"UPDATE users SET email = ? WHERE id = ?"
);
stmt.setString(1, email);
stmt.setLong(2, userId);
stmt.executeUpdate();
// BUG: If exception occurs, connection leaks!
// Should be in finally block
stmt.close();
conn.close();
}
// SECURE - try-with-resources ensures cleanup
public void updateUserSafe(Long userId, String email) throws SQLException {
String sql = "UPDATE users SET email = ? WHERE id = ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, email);
stmt.setLong(2, userId);
stmt.executeUpdate();
} // Automatic cleanup even if exception occurs
}
// SECURE - manual cleanup in finally (Java 6 compatible)
public void updateUserManualCleanup(Long userId, String email)
throws SQLException {
Connection conn = null;
PreparedStatement stmt = null;
try {
conn = dataSource.getConnection();
stmt = conn.prepareStatement(
"UPDATE users SET email = ? WHERE id = ?"
);
stmt.setString(1, email);
stmt.setLong(2, userId);
stmt.executeUpdate();
} finally {
// Close in reverse order of creation
if (stmt != null) {
try { stmt.close(); } catch (SQLException e) { /* log */ }
}
if (conn != null) {
try { conn.close(); } catch (SQLException e) { /* log */ }
}
}
}
}
Verify Connection Pool Configuration
Manual verification steps:
Check DataSource configuration
Verify that your application uses DataSource from JNDI or dependency injection, never DriverManager.getConnection()
# Search for DriverManager usage (should find zero results)
grep -r "DriverManager.getConnection" src/
# Verify DataSource is injected or looked up via JNDI
grep -r "@Autowired.*DataSource\|@Resource.*DataSource\|lookup.*jdbc" src/
Verify connection pooling is active
Check that a connection pool library is configured (HikariCP, DBCP2, or container-managed)
# For Spring Boot - check application.properties/yaml
grep -E "spring.datasource.hikari|spring.datasource.dbcp2" application.properties
# For Java EE - check server configuration (context.xml, standalone.xml)
cat context.xml | grep -A 10 "<Resource.*DataSource"
Test connection acquisition
Verify connections can be obtained and returned to the pool
// Quick smoke test - should complete in <100ms
try (Connection conn = dataSource.getConnection()) {
System.out.println("Connection class: " + conn.getClass().getName());
// Should be a proxy/wrapper, not direct JDBC connection
// Examples: HikariProxyConnection, PooledConnection
}
Monitor pool metrics
Use JMX, actuator endpoints, or logging to verify pool behavior
// Spring Boot Actuator - enable metrics endpoint
// application.properties:
// management.endpoints.web.exposure.include=health,metrics
// management.endpoint.metrics.enabled=true
// Then access: http://localhost:8080/actuator/metrics/hikaricp.connections.active
Load testing
Use tools like JMeter or Gatling to simulate concurrent requests
# Example with Apache Bench
ab -n 1000 -c 50 http://localhost:8080/api/users
# Monitor connection pool during load:
# - Active connections should stay below max pool size
# - No connection timeout errors
# - Connections reused (not created per request)
Verify no hard-coded credentials
Ensure credentials come from environment variables or secrets management
# Search for hard-coded credentials (should find zero)
grep -ri "password.*=.*\"[^\$]" src/ config/
grep -ri "jdbc:.*://.*:[^@]*@" src/
# Verify environment variables are set
echo $DB_URL $DB_USERNAME $DB_PASSWORD
Automated verification with static analysis
# Use SpotBugs, PMD, or SonarQube to detect:
# - DriverManager usage
# - Hard-coded credentials
# - Missing try-with-resources for connections
mvn spotbugs:check
mvn pmd:check
Connection leak detection
Enable HikariCP leak detection in development
# application.properties
spring.datasource.hikari.leak-detection-threshold=60000 # 60 seconds
# Warns if connection held > 60s without being closed
Common Vulnerable Patterns
DriverManager with Hard-Coded Credentials
public class UserDAO {
public User findUser(String username) throws SQLException {
// VULNERABLE - Hard-coded credentials in code
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/mydb",
"root",
"password123" // Hard-coded password!
);
PreparedStatement stmt = conn.prepareStatement(
"SELECT * FROM users WHERE username = ?"
);
stmt.setString(1, username);
ResultSet rs = stmt.executeQuery();
// Process results...
User user = null;
if (rs.next()) {
user = new User(rs.getString("username"), rs.getString("email"));
}
// Connection never closed - LEAK!
return user;
}
}
Why is this vulnerable: DriverManager.getConnection() creates a direct database connection without pooling, requiring hard-coded credentials in the source code. Every call creates a new physical connection (expensive TCP handshake, authentication), and credentials end up in compiled bytecode (visible in decompiled JAR files) and version control. If credentials change, code must be recompiled and redeployed. Additionally, the connection is never closed, causing resource leaks - each call consumes a database connection slot until the database hits its connection limit and refuses new connections (DoS). Without connection pooling, performance degrades under load as each request waits for a new connection to be established.
Connection Not Properly Closed (Resource Leak)
public class OrderService {
public void updateOrder(Long orderId, String status) throws SQLException {
Connection conn = null;
PreparedStatement stmt = null;
try {
conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
stmt = conn.prepareStatement(
"UPDATE orders SET status = ? WHERE id = ?"
);
stmt.setString(1, status);
stmt.setLong(2, orderId);
stmt.executeUpdate();
// VULNERABLE - cleanup only in try block
stmt.close();
conn.close();
} catch (SQLException e) {
// BUG: If exception occurs, connection leaks!
throw e;
}
// No finally block - connection never closed on error
}
}
Why is this vulnerable: Connections are only closed in the success path (after executeUpdate()), but if any SQL exception occurs (network failure, constraint violation, timeout), the catch block re-throws without closing resources. The connection remains open indefinitely, consuming a connection slot in the database. Under heavy load or with intermittent errors, connections accumulate until the database reaches max_connections and rejects new requests. This causes cascading failures - even valid requests fail because the connection pool is exhausted. The JVM garbage collector won't reclaim unclosed connections (they're not eligible for GC while referenced), so the leak persists until application restart.
No Connection Pooling (Performance Degradation)
@RestController
public class ProductController {
@GetMapping("/api/products/{id}")
public Product getProduct(@PathVariable Long id) throws SQLException {
// VULNERABLE - New connection per request!
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();
if (rs.next()) {
return new Product(
rs.getLong("id"),
rs.getString("name"),
rs.getBigDecimal("price")
);
}
return null;
} finally {
conn.close(); // Closes physical connection, not returned to pool
}
}
}
Why is this vulnerable: Each HTTP request creates a new physical database connection (TCP handshake, TLS negotiation, authentication, session setup), taking 50-200ms before the query even executes. Under load (100 requests/second), this creates 100 new connections/second, overwhelming the database with connection overhead. The database must maintain separate buffers, locks, and state for each connection, consuming memory and CPU. When the connection closes, all that work is discarded. Connection pooling reuses existing connections, reducing overhead to <1ms. Without pooling, response times degrade from milliseconds to seconds under moderate load, and the application cannot scale beyond a few dozen concurrent users.
Mixed Transaction Management
public class TransferService {
public void transferFunds(Long fromId, Long toId, BigDecimal amount)
throws SQLException {
// VULNERABLE - Manual transaction management
Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
try {
conn.setAutoCommit(false); // Start transaction
// Deduct from source account
try (PreparedStatement stmt = conn.prepareStatement(
"UPDATE accounts SET balance = balance - ? WHERE id = ?")) {
stmt.setBigDecimal(1, amount);
stmt.setLong(2, fromId);
stmt.executeUpdate();
}
// Add to destination account
try (PreparedStatement stmt = conn.prepareStatement(
"UPDATE accounts SET balance = balance + ? WHERE id = ?")) {
stmt.setBigDecimal(1, amount);
stmt.setLong(2, toId);
stmt.executeUpdate();
}
conn.commit(); // Commit transaction
} catch (SQLException e) {
conn.rollback(); // BUG: What if rollback fails?
throw e;
} finally {
conn.setAutoCommit(true); // BUG: What if this fails?
conn.close();
}
}
}
Why is this vulnerable: Manual transaction management with direct connections is error-prone and bypasses container-managed transactions. If rollback() fails (network issue, database crash), the application doesn't know whether the transaction committed or rolled back (inconsistent state). If setAutoCommit(true) fails, the connection returns to the pool (if pooled) with autoCommit still disabled, affecting subsequent requests unpredictably. Container-managed transactions (JTA, Spring @Transactional) handle these edge cases, provide distributed transaction support, and automatically manage transaction boundaries across multiple operations. Direct connection management forces every developer to reimplement transaction handling, leading to inconsistencies and bugs.
Credentials in Configuration Files (Version Control Exposure)
<!-- VULNERABLE - persistence.xml with hard-coded credentials -->
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence" version="2.1">
<persistence-unit name="myapp">
<properties>
<property name="javax.persistence.jdbc.url"
value="jdbc:mysql://localhost:3306/myapp"/>
<property name="javax.persistence.jdbc.user"
value="dbadmin"/>
<property name="javax.persistence.jdbc.password"
value="MySecretPassword123"/> <!-- EXPOSED! -->
<property name="hibernate.dialect"
value="org.hibernate.dialect.MySQL8Dialect"/>
</properties>
</persistence-unit>
</persistence>
Why is this vulnerable: Configuration files with hard-coded credentials get committed to version control (Git, SVN), exposing passwords to everyone with repository access. Even if deleted in a later commit, credentials remain in Git history forever (accessible via git log, git show). Developers clone repositories to laptops (often unencrypted), share access with contractors, and push to public GitHub repos accidentally. Once credentials leak, attackers can access the database directly, bypassing application security controls. Credential rotation requires code changes and redeployment (slow, error-prone). Environment-specific credentials (dev, staging, production) can't use the same configuration file, leading to copy-paste errors and production credentials in development.
Secure Patterns
Container-Managed DataSource with JNDI
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class UserDAOSecure {
private final DataSource dataSource;
public UserDAOSecure() {
try {
InitialContext ctx = new InitialContext();
this.dataSource = (DataSource) ctx.lookup("java:comp/env/jdbc/MyDB");
} catch (NamingException e) {
throw new RuntimeException("Failed to lookup DataSource", e);
}
}
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()) {
if (rs.next()) {
return new User(
rs.getLong("id"),
rs.getString("username"),
rs.getString("email")
);
}
return null;
}
} // Connection automatically returned to pool
}
}
Why this works: Container-managed DataSource from JNDI provides connection pooling (reuses connections, no creation overhead), credential management (passwords in container config, not code), automatic resource cleanup (connections returned to pool even on exception with try-with-resources), and centralized configuration (change pool settings without code changes). The container (Tomcat, WildFly) handles connection lifecycle, validation, and leak detection. Credentials are externalized to context.xml or environment variables, never in source code or version control.
Spring Boot with Auto-Configured Connection Pool
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class ProductRepository {
@Autowired
private JdbcTemplate jdbcTemplate; // Uses HikariCP pool automatically
public Product findById(Long id) {
return jdbcTemplate.queryForObject(
"SELECT * FROM products WHERE id = ?",
new Object[]{id},
(rs, rowNum) -> new Product(
rs.getLong("id"),
rs.getString("name"),
rs.getBigDecimal("price")
)
);
}
public void updatePrice(Long id, BigDecimal newPrice) {
jdbcTemplate.update(
"UPDATE products SET price = ? WHERE id = ?",
newPrice, id
);
}
}
Why this works: Spring Boot auto-configures HikariCP (fastest Java connection pool) based on application.properties - no manual DataSource creation needed. JdbcTemplate handles connection acquisition, statement preparation, parameter binding, result mapping, and connection return automatically. Connections are never leaked (template closes them), transactions integrate with @Transactional, and credentials come from externalized configuration (environment variables, application.yaml). HikariCP provides connection validation, leak detection, and JMX monitoring out of the box.
Proper Resource Management with Try-With-Resources
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class OrderService {
private final DataSource dataSource;
public OrderService(DataSource dataSource) {
this.dataSource = dataSource;
}
public void updateOrderStatus(Long orderId, String status) throws SQLException {
String sql = "UPDATE orders SET status = ?, updated_at = NOW() WHERE id = ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, status);
stmt.setLong(2, orderId);
int rowsUpdated = stmt.executeUpdate();
if (rowsUpdated == 0) {
throw new SQLException("Order not found: " + orderId);
}
} // Automatic cleanup even if exception occurs
}
}
Why this works: Try-with-resources (Java 7+) guarantees that Connection, PreparedStatement, and ResultSet are closed even if exceptions occur. Resources are closed in reverse order of creation (inner to outer). If executeUpdate() throws SQLException, the JVM calls stmt.close() then conn.close() automatically before propagating the exception. This prevents resource leaks in all code paths (success, exception, early return). The connection returns to the pool immediately, available for reuse by other requests.
Externalized Configuration with Environment Variables
# NO hard-coded credentials
spring:
datasource:
url: ${DB_URL:jdbc:postgresql://localhost:5432/myapp}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
driver-class-name: org.postgresql.Driver
hikari:
maximum-pool-size: ${DB_POOL_SIZE:20}
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
leak-detection-threshold: 60000
# Set environment variables (not in code!)
export DB_URL="jdbc:postgresql://prod-db.example.com:5432/myapp"
export DB_USERNAME="app_user"
export DB_PASSWORD="$(vault read -field=password secret/db/myapp)"
export DB_POOL_SIZE="50"
# Run application
java -jar myapp.jar
Why this works: Credentials are externalized to environment variables, never in source code or version control. Different environments (dev, staging, prod) use different credentials without code changes. Secrets management tools (HashiCorp Vault, AWS Secrets Manager) rotate credentials automatically, and the application picks up changes without redeployment (if configured to refresh). Environment variables are not committed to Git, not visible in compiled bytecode, and have OS-level access controls. Configuration values have defaults (${VAR:default}) for local development.
Security Checklist
- No DriverManager.getConnection() in application code
- Using DataSource from JNDI or dependency injection
- Connection pooling configured (HikariCP, DBCP, or container)
- No hard-coded database credentials
- Credentials externalized (environment variables, vault)
- try-with-resources used for all connections
- Pool size configured appropriately
- Connection validation enabled
- Leak detection enabled in development