Hängender Datenbank-Cursor (Cursor-Injection)
Beschreibung
Hängender Datenbank-Cursor tritt auf, wenn ein Datenbank-Cursor nach seiner beabsichtigten Verwendung offen gelassen wird, oder wenn Cursor-Referenzen von Angreifern manipuliert werden können. Datenbank-Cursor sind Pointer auf Ergebnismengen, die einen Zustand beibehalten. Wenn Cursor-Namen vorhersehbar oder benutzerkontrolliert sind, können Angreifer möglicherweise auf Cursor zugreifen oder diese manipulieren, die von anderen Sitzungen erstellt wurden, was potenziell unbefugten Datenzugriff oder Störung von Datenbankoperationen ermöglicht.
Risiko
Vorhersehbare Cursor-Namen ermöglichen Angreifern den Zugriff auf Abfrageergebnisse anderer Benutzer. Hängende Cursor verbrauchen Datenbankressourcen und ermöglichen Denial of Service. In einigen Datenbanken kann Cursor-Injection zu Datenlecks über Sitzungen hinweg führen. Speichererschöpfung durch ungeschlossene Cursor beeinträchtigt die Datenbankleistung. Race Conditions mit Cursorn können Daten aus gleichzeitigen Transaktionen exponieren.
Lösung
Schließen Sie Datenbank-Cursor immer nach der Verwendung. Verwenden Sie eindeutige, unvorhersehbare Cursor-Namen, wenn Namen sichtbar sind. Implementieren Sie ordnungsgemäßes Cursor-Lifecycle-Management. Verwenden Sie parametrisierte Cursor-Namen, wenn dynamisch. Stellen Sie sicher, dass Cursor nach Möglichkeit transaktionsbezogen sind. Implementieren Sie Ressourcenlimits für die Cursor-Anzahl. Verwenden Sie Connection-Pooling mit ordnungsgemäßer Cursor-Bereinigung.
Häufige Auswirkungen
| Auswirkung | Details |
|---|---|
| Vertraulichkeit | Bereich: Datenleck Zugriff auf Cursor anderer Sitzungen exponiert deren Daten. |
| Verfügbarkeit | Bereich: Ressourcenerschöpfung Ungeschlossene Cursor verbrauchen Datenbankressourcen. |
| Integrität | Bereich: Datenmanipulation Modifikation des Cursor-Zustands beeinflusst Abfrageergebnisse. |
Beispielcode + Lösungscode
Verwundbarer Code
// VERWUNDBAR: Cursor wird nie geschlossen
public class VulnerableDAO {
public List<User> getAllUsers() throws SQLException {
Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
// Cursor geöffnet aber nie geschlossen
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
List<User> users = new ArrayList<>();
while (rs.next()) {
users.add(mapUser(rs));
}
// Fehlt: rs.close(), stmt.close(), conn.close()
return users;
}
}
// VERWUNDBAR: Cursor bleibt bei Ausnahme offen
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()) {
// Wenn hier Ausnahme, bleibt Cursor offen
users.add(processUser(rs)); // Kann werfen
}
rs.close();
stmt.close();
conn.close();
return users;
}
// VERWUNDBAR: Vorhersehbarer 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 = ?"
);
// Angreifer kann Cursor-Namen erraten: cursor_12345
// Kann in anderer Sitzung zugreifen
}
-- VERWUNDBAR: PL/SQL mit vorhersehbarem Cursor
CREATE OR REPLACE PROCEDURE get_user_data(p_user_id IN NUMBER) AS
-- Vorhersehbarer Cursor-Name
CURSOR user_cursor IS
SELECT * FROM sensitive_data WHERE user_id = p_user_id;
BEGIN
OPEN user_cursor;
-- Cursor bleibt offen wenn Ausnahme auftritt
FETCH user_cursor INTO v_record;
-- Fehlt: CLOSE user_cursor im Exception-Handler
END;
/
-- VERWUNDBAR: Dynamischer Cursor mit Benutzereingabe
CREATE OR REPLACE PROCEDURE dynamic_query(p_cursor_name VARCHAR2) AS
v_cursor INTEGER;
BEGIN
-- Angreifer kontrolliert Cursor-Name
v_cursor := DBMS_SQL.OPEN_CURSOR;
-- ...
-- Cursor wird möglicherweise nicht geschlossen
END;
/
# VERWUNDBAR: Python mit ungeschlossenen Cursorn
import psycopg2
def get_users_vulnerable():
conn = psycopg2.connect(database_url)
cursor = conn.cursor() # Cursor geöffnet
cursor.execute("SELECT * FROM users")
users = cursor.fetchall()
# Cursor nie geschlossen!
# Verbindung nie geschlossen!
return users
# VERWUNDBAR: Benannter Cursor mit vorhersehbarem Namen
def process_data_vulnerable(user_id):
conn = psycopg2.connect(database_url)
# Vorhersehbarer Cursor-Name
cursor = conn.cursor(name=f"cursor_{user_id}")
cursor.execute("SELECT * FROM sensitive_data")
# Bei Ausnahme bleibt Cursor offen
for row in cursor:
process(row)
# VERWUNDBAR: Cursor-Akkumulation
class VulnerableBatchProcessor:
def __init__(self):
self.conn = psycopg2.connect(database_url)
self.cursors = []
def process_batch(self, batch_id):
# Erstellt jeden Aufruf neuen Cursor, schließt nie
cursor = self.conn.cursor(name=f"batch_{batch_id}")
cursor.execute(f"SELECT * FROM batch_data WHERE id = {batch_id}")
self.cursors.append(cursor) # Akkumuliert für immer
Lösungscode
// SICHER: Ordnungsgemäßer Cursor-Lifecycle mit 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));
}
} // Alle Ressourcen automatisch geschlossen
return users;
}
}
// SICHER: Manuelle Ressourcenverwaltung mit 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 {
// In umgekehrter Reihenfolge schließen, jede Ausnahme behandeln
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); }
}
}
// SICHER: Eindeutige Cursor-Namen
public void processDataSafe(String userId) throws SQLException {
// Unvorhersehbaren Cursor-Namen generieren
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();
// Ergebnisse verarbeiten
}
// Cursor automatisch geschlossen
}
-- SICHER: PL/SQL mit ordnungsgemäßem 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
-- Cursor immer bei Fehler schließen
IF user_cursor%ISOPEN THEN
CLOSE user_cursor;
END IF;
RAISE;
END;
CLOSE user_cursor;
END;
/
-- SICHER: Verwendung von implizitem Cursor (automatisch geschlossen)
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;
-- Impliziter Cursor automatisch geschlossen
END;
/
# SICHER: Python mit Context-Managern
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()
# Beide Cursor und Verbindung automatisch geschlossen
# SICHER: Benannter Cursor mit eindeutigem Namen und ordnungsgemäßer Bereinigung
import uuid
def process_data_safe(user_id):
# Unvorhersehbarer 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 beim Verlassen geschlossen
# SICHER: Batch-Prozessor mit Cursor-Bereinigung
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 automatisch geschlossen
def close(self):
self.conn.close()
def __enter__(self):
return self
def __exit__(self, *args):
self.close()
# Verwendung
with SafeBatchProcessor() as processor:
processor.process_batch(123)
Ausgenutzt in der Praxis
Datenbank-Ressourcenerschöpfung
Anwendungen mit Cursor-Lecks verursachten Datenbankausfälle unter Last.
Sitzungsübergreifender Datenzugriff
Vorhersehbare Cursor-Namen ermöglichten Zugriff auf Abfrageergebnisse anderer Sitzungen.
Speichererschöpfungs-Angriffe
Angreifer lösten Cursor-Erstellung ohne Schließung aus, um Speicher zu erschöpfen.
Tools zum Testen/Ausnutzen
- Datenbank-Monitoring-Tools für Cursor-Lecks.
- Lasttests um Cursor-Erschöpfung aufzudecken.
- Benutzerdefinierte Skripte zum Testen der Cursor-Namen-Vorhersehbarkeit.
CVE-Beispiele
- CVEs durch Datenbank-Cursor-Ressourcenerschöpfung.
- Datenlecks durch Cursor-Manipulation.
Referenzen
- MITRE. "CWE-619: Dangling Database Cursor (Cursor Injection)." https://cwe.mitre.org/data/definitions/619.html
- Datenbank-Anbieter-Dokumentation zum Cursor-Management.