Dangling Database Cursor (Cursor Injection)

Description

Dangling Database Cursor occurs when a database cursor is left open after its intended use, or when cursor references can be manipulated by attackers. Database cursors are pointers to result sets that maintain state. If cursor names are predictable or user-controlled, attackers may be able to access or manipulate cursors created by other sessions, potentially accessing unauthorized data or interfering with database operations.

Risk

Predictable cursor names allow attackers to access other users' query results. Dangling cursors consume database resources, enabling denial of service. In some databases, cursor injection can lead to data leakage across sessions. Memory exhaustion from unclosed cursors affects database performance. Race conditions with cursors can expose data from concurrent transactions.

Solution

Always close database cursors when done. Use unique, unpredictable cursor names if names are visible. Implement proper cursor lifecycle management. Use parameterized cursor names if dynamic. Ensure cursors are transaction-scoped where possible. Implement resource limits on cursor count. Use connection pooling with proper cursor cleanup.

Common Consequences

ImpactDetails
ConfidentialityScope: Data Leakage

Accessing other sessions' cursors exposes their data.
AvailabilityScope: Resource Exhaustion

Unclosed cursors consume database resources.
IntegrityScope: Data Manipulation

Modifying cursor state affects query results.

Example Code + Solution Code

Vulnerable Code

// VULNERABLE: Never closing cursor
public class VulnerableDAO {

    public List<User> getAllUsers() throws SQLException {
        Connection conn = dataSource.getConnection();
        Statement stmt = conn.createStatement();

        // Cursor opened but never closed
        ResultSet rs = stmt.executeQuery("SELECT * FROM users");

        List<User> users = new ArrayList<>();
        while (rs.next()) {
            users.add(mapUser(rs));
        }

        // Missing: rs.close(), stmt.close(), conn.close()
        return users;
    }
}

// VULNERABLE: Cursor left open on exception
public List<User> getUsersVulnerable() throws SQLException {
    Connection conn = dataSource.getConnection();
    Statement stmt = conn.createStatement();
    ResultSet rs = stmt.executeQuery("SELECT * FROM users");

    List<User> users = new ArrayList<>();
    while (rs.next()) {
        // If exception here, cursor stays open
        users.add(processUser(rs));  // May throw
    }

    rs.close();
    stmt.close();
    conn.close();
    return users;
}

// VULNERABLE: Predictable cursor name (stored procedure)
public void processDataVulnerable(String userId) throws SQLException {
    CallableStatement cs = conn.prepareCall(
        "DECLARE cursor_" + userId + " CURSOR FOR " +
        "SELECT * FROM user_data WHERE user_id = ?"
    );

    // Attacker can guess cursor name: cursor_12345
    // May access in another session
}
-- VULNERABLE: PL/SQL with predictable cursor
CREATE OR REPLACE PROCEDURE get_user_data(p_user_id IN NUMBER) AS
    -- Predictable cursor name
    CURSOR user_cursor IS
        SELECT * FROM sensitive_data WHERE user_id = p_user_id;
BEGIN
    OPEN user_cursor;
    -- Cursor left open if exception occurs
    FETCH user_cursor INTO v_record;

    -- Missing: CLOSE user_cursor in exception handler
END;
/

-- VULNERABLE: Dynamic cursor with user input
CREATE OR REPLACE PROCEDURE dynamic_query(p_cursor_name VARCHAR2) AS
    v_cursor INTEGER;
BEGIN
    -- Attacker controls cursor name
    v_cursor := DBMS_SQL.OPEN_CURSOR;
    -- ...
    -- Cursor may not be closed
END;
/
# VULNERABLE: Python with unclosed cursors
import psycopg2

def get_users_vulnerable():
    conn = psycopg2.connect(database_url)
    cursor = conn.cursor()  # Cursor opened

    cursor.execute("SELECT * FROM users")
    users = cursor.fetchall()

    # Cursor never closed!
    # Connection never closed!
    return users

# VULNERABLE: Named cursor with predictable name
def process_data_vulnerable(user_id):
    conn = psycopg2.connect(database_url)

    # Predictable cursor name
    cursor = conn.cursor(name=f"cursor_{user_id}")
    cursor.execute("SELECT * FROM sensitive_data")

    # If exception, cursor remains open
    for row in cursor:
        process(row)

# VULNERABLE: Cursor accumulation
class VulnerableBatchProcessor:
    def __init__(self):
        self.conn = psycopg2.connect(database_url)
        self.cursors = []

    def process_batch(self, batch_id):
        # Creates new cursor each call, never closes
        cursor = self.conn.cursor(name=f"batch_{batch_id}")
        cursor.execute(f"SELECT * FROM batch_data WHERE id = {batch_id}")
        self.cursors.append(cursor)  # Accumulates forever

Fixed Code

// SAFE: Proper cursor lifecycle with try-with-resources
public class SafeDAO {

    public List<User> getAllUsers() throws SQLException {
        List<User> users = new ArrayList<>();

        try (Connection conn = dataSource.getConnection();
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery("SELECT * FROM users")) {

            while (rs.next()) {
                users.add(mapUser(rs));
            }
        }  // All resources automatically closed

        return users;
    }
}

// SAFE: Manual resource management with finally
public List<User> getUsersSafe() throws SQLException {
    Connection conn = null;
    Statement stmt = null;
    ResultSet rs = null;

    try {
        conn = dataSource.getConnection();
        stmt = conn.createStatement();
        rs = stmt.executeQuery("SELECT * FROM users");

        List<User> users = new ArrayList<>();
        while (rs.next()) {
            users.add(processUser(rs));
        }
        return users;

    } finally {
        // Close in reverse order, handle each exception
        if (rs != null) try { rs.close(); } catch (SQLException e) { log(e); }
        if (stmt != null) try { stmt.close(); } catch (SQLException e) { log(e); }
        if (conn != null) try { conn.close(); } catch (SQLException e) { log(e); }
    }
}

// SAFE: Unique cursor names
public void processDataSafe(String userId) throws SQLException {
    // Generate unpredictable cursor name
    String cursorName = "cursor_" + UUID.randomUUID().toString().replace("-", "");

    try (CallableStatement cs = conn.prepareCall(
            "DECLARE " + cursorName + " CURSOR FOR " +
            "SELECT * FROM user_data WHERE user_id = ?")) {

        cs.setString(1, userId);
        cs.execute();
        // Process results
    }
    // Cursor automatically closed
}
-- SAFE: PL/SQL with proper cursor management
CREATE OR REPLACE PROCEDURE get_user_data(p_user_id IN NUMBER) AS
    CURSOR user_cursor IS
        SELECT * FROM sensitive_data WHERE user_id = p_user_id;
    v_record user_cursor%ROWTYPE;
BEGIN
    OPEN user_cursor;

    BEGIN
        LOOP
            FETCH user_cursor INTO v_record;
            EXIT WHEN user_cursor%NOTFOUND;
            process_record(v_record);
        END LOOP;
    EXCEPTION
        WHEN OTHERS THEN
            -- Always close cursor on error
            IF user_cursor%ISOPEN THEN
                CLOSE user_cursor;
            END IF;
            RAISE;
    END;

    CLOSE user_cursor;
END;
/

-- SAFE: Using implicit cursor (auto-closes)
CREATE OR REPLACE PROCEDURE get_user_data_safe(p_user_id IN NUMBER) AS
BEGIN
    FOR rec IN (SELECT * FROM sensitive_data WHERE user_id = p_user_id)
    LOOP
        process_record(rec);
    END LOOP;
    -- Implicit cursor automatically closed
END;
/
# SAFE: Python with context managers
import psycopg2
from contextlib import contextmanager

@contextmanager
def get_cursor(conn):
    cursor = conn.cursor()
    try:
        yield cursor
    finally:
        cursor.close()

def get_users_safe():
    with psycopg2.connect(database_url) as conn:
        with conn.cursor() as cursor:
            cursor.execute("SELECT * FROM users")
            return cursor.fetchall()
    # Both cursor and connection closed automatically

# SAFE: Named cursor with unique name and proper cleanup
import uuid

def process_data_safe(user_id):
    # Unpredictable cursor name
    cursor_name = f"cursor_{uuid.uuid4().hex}"

    with psycopg2.connect(database_url) as conn:
        with conn.cursor(name=cursor_name) as cursor:
            cursor.execute(
                "SELECT * FROM sensitive_data WHERE user_id = %s",
                (user_id,)
            )
            for row in cursor:
                process(row)
    # Cursor closed on exit

# SAFE: Batch processor with cursor cleanup
class SafeBatchProcessor:
    def __init__(self):
        self.conn = psycopg2.connect(database_url)

    def process_batch(self, batch_id):
        cursor_name = f"batch_{uuid.uuid4().hex}"

        with self.conn.cursor(name=cursor_name) as cursor:
            cursor.execute(
                "SELECT * FROM batch_data WHERE id = %s",
                (batch_id,)
            )
            for row in cursor:
                self.process_row(row)
        # Cursor automatically closed

    def close(self):
        self.conn.close()

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.close()

# Usage
with SafeBatchProcessor() as processor:
    processor.process_batch(123)

Exploited in the Wild

Database Resource Exhaustion

Applications with cursor leaks caused database outages under load.

Cross-Session Data Access

Predictable cursor names allowed accessing other sessions' query results.

Memory Exhaustion Attacks

Attackers triggered cursor creation without closure to exhaust memory.


Tools to test/exploit

  • Database monitoring tools for cursor leaks.

  • Load testing to reveal cursor exhaustion.

  • Custom scripts to test cursor name predictability.


CVE Examples

  • CVEs from database cursor resource exhaustion.

  • Data leakage through cursor manipulation.


References

  1. MITRE. "CWE-619: Dangling Database Cursor (Cursor Injection)." https://cwe.mitre.org/data/definitions/619.html

  2. Database vendor documentation on cursor management.