Skip to content

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

application.yml
# 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

Additional Resources