Autorisierungsumgehung durch benutzergesteuerten SQL-Primärschlüssel
Beschreibung
Autorisierungsumgehung durch benutzergesteuerten SQL-Primärschlüssel ist eine Schwachstelle, bei der eine Anwendung Benutzern erlaubt, Primärschlüsselwerte anzugeben, die in Datenbankabfragen verwendet werden, ohne ordnungsgemäße Autorisierungsverifizierung, was den Zugriff auf Datensätze ermöglicht, die sie nicht einsehen oder ändern dürfen sollten. Dies tritt auf, wenn drei Bedingungen zusammentreffen: nicht vertrauenswürdige Benutzereingabe gelangt in die Anwendung, diese Eingabe spezifiziert einen Primärschlüsselwert in SQL-Abfragen, und die Anwendung verifiziert nicht, dass der Benutzer die Berechtigung hat, auf den angegebenen Datensatz zuzugreifen. Angreifer nutzen dies aus, indem sie den Primärschlüsselparameter modifizieren, um auf Daten anderer Benutzer zuzugreifen.
Risiko
Schwachstellen durch benutzergesteuerte Primärschlüssel führen zu horizontaler Privilegieneskalation, bei der Benutzer auf Daten anderer Benutzer auf der gleichen Privilegienebene zugreifen können. Angreifer können Datensatz-IDs aufzählen, um systematisch auf alle Datensätze in einer Tabelle zuzugreifen. Sensible persönliche Informationen, Finanzdaten, medizinische Daten oder vertrauliche Geschäftsinformationen können offengelegt werden. Über das Lesen nicht autorisierter Daten hinaus können Angreifer möglicherweise Datensätze anderer Benutzer modifizieren oder löschen, wenn die Anwendung Aktualisierungen basierend auf dem gleichen nicht validierten Schlüssel erlaubt. Diese Schwachstelle ist besonders gefährlich, weil sie oft Zugriffkontrollen auf Anwendungsebene vollständig umgeht, da die Datenbank kein Wissen darüber hat, auf welche Datensätze ein Benutzer zugreifen sollte.
Lösung
Implementieren Sie ordnungsgemäße Autorisierungsprüfungen, die verifizieren, dass der aktuelle Benutzer die Berechtigung hat, auf den angeforderten Datensatz zuzugreifen, bevor Datenbankoperationen ausgeführt werden. Speichern Sie Eigentums- oder Zugriffskontrollinformationen mit jedem Datensatz und verifizieren Sie diese serverseitig. Verwenden Sie indirekte Referenzen, die benutzersichtbare Identifikatoren auf tatsächliche Datenbankschlüssel abbilden und so direkte Manipulation verhindern. Implementieren Sie parametrisierte Abfragen, um SQL-Injection zu verhindern, während Sie auch Autorisierungslogik hinzufügen. Erwägen Sie die Verwendung von Zeilensicherheitsfunktionen, die in modernen Datenbanken verfügbar sind. Protokollieren Sie alle Datenzugriffsversuche für Audit-Zwecke. Vertrauen Sie niemals benutzerbereitgestellten Primärschlüsseln ohne Autorisierungsverifizierung.
Häufige Auswirkungen
| Auswirkung | Details |
|---|---|
| Vertraulichkeit | Bereich: Vertraulichkeit Anwendungsdaten lesen - Angreifer können auf sensible Daten anderer Benutzer zugreifen, indem sie Primärschlüsselwerte manipulieren, um auf nicht autorisierte Datensätze zu zeigen. |
| Integrität | Bereich: Integrität Anwendungsdaten ändern - Wenn die Anwendung Aktualisierungen mit benutzergesteuerten Schlüsseln erlaubt, können Angreifer Daten anderer Benutzer modifizieren oder löschen. |
| Zugriffskontrolle | Bereich: Zugriffskontrolle Schutzmechanismus umgehen - Autorisierungskontrollen werden umgangen, weil die Datenbankabfrage jeden Datensatz abruft, der dem angegebenen Schlüssel entspricht, unabhängig vom Eigentum. |
Beispielcode
Verwundbarer Code
// Verwundbar: C# ASP.NET zeigt Rechnung ohne Autorisierungsprüfung an
using System.Data.SqlClient;
using System.Web.UI;
public partial class VulnerableInvoice : Page
{
protected void Page_Load(object sender, EventArgs e)
{
// Verwundbar: Rechnungs-ID aus Benutzeranfrage
string invoiceId = Request.QueryString["id"];
string connectionString = ConfigurationManager.ConnectionStrings["DB"].ConnectionString;
using (SqlConnection conn = new SqlConnection(connectionString))
{
// Verwundbar: Parametrisierte Abfrage aber KEINE Autorisierungsprüfung
string query = "SELECT * FROM invoices WHERE invoice_id = @id";
SqlCommand cmd = new SqlCommand(query, conn);
cmd.Parameters.AddWithValue("@id", invoiceId);
conn.Open();
SqlDataReader reader = cmd.ExecuteReader();
// Verwundbar: Zeigt jede Rechnung unabhängig vom Eigentum an
// Angreifer kann ?id=123 zu ?id=456 ändern um andere Rechnungen zu sehen
if (reader.Read())
{
lblInvoiceNumber.Text = reader["invoice_number"].ToString();
lblAmount.Text = reader["amount"].ToString();
lblCustomerName.Text = reader["customer_name"].ToString();
}
}
}
}
// Verwundbar: Java Servlet mit IDOR-Schwachstelle
import javax.servlet.http.*;
import java.sql.*;
public class VulnerableOrderServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// Verwundbar: Bestell-ID aus Benutzereingabe
String orderId = request.getParameter("orderId");
try (Connection conn = dataSource.getConnection()) {
// Verwundbar: Keine Prüfung ob Benutzer diese Bestellung besitzt
String sql = "SELECT * FROM orders WHERE order_id = ?";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setString(1, orderId);
ResultSet rs = stmt.executeQuery();
// Verwundbar: Gibt jede Bestellung zurück, nicht nur Benutzerbestellungen
// Benutzer kann auf jede Bestellung durch Ändern des orderId-Parameters zugreifen
if (rs.next()) {
Order order = new Order();
order.setId(rs.getLong("order_id"));
order.setCustomerId(rs.getLong("customer_id"));
order.setAmount(rs.getBigDecimal("amount"));
order.setItems(rs.getString("items"));
request.setAttribute("order", order);
request.getRequestDispatcher("/order.jsp").forward(request, response);
}
} catch (SQLException e) {
throw new ServletException(e);
}
}
// Verwundbar: Aktualisierung ohne Eigentumsprüfung
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String orderId = request.getParameter("orderId");
String newStatus = request.getParameter("status");
try (Connection conn = dataSource.getConnection()) {
// Verwundbar: Kann jede Bestellung aktualisieren, nicht nur eigene
String sql = "UPDATE orders SET status = ? WHERE order_id = ?";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setString(1, newStatus);
stmt.setString(2, orderId);
stmt.executeUpdate();
// Angreifer kann Bestellungen anderer Benutzer stornieren oder ändern!
} catch (SQLException e) {
throw new ServletException(e);
}
}
}
<?php
// Verwundbar: PHP Benutzerprofilzugriff
function get_user_profile($user_id) {
$conn = get_database_connection();
// Verwundbar: Keine Autorisierungsprüfung
$stmt = $conn->prepare("SELECT * FROM users WHERE user_id = ?");
$stmt->bind_param("i", $user_id);
$stmt->execute();
// Angreifer kann jedes Benutzerprofil durch Ändern der ID anzeigen
return $stmt->get_result()->fetch_assoc();
}
// Verwundbar: Profilseite
$profile_id = $_GET['id']; // Benutzergesteuert
$profile = get_user_profile($profile_id);
// Verwundbar: Zeigt potenziell Daten anderer Benutzer an
echo "Name: " . htmlspecialchars($profile['name']);
echo "E-Mail: " . htmlspecialchars($profile['email']);
echo "SSN: " . htmlspecialchars($profile['ssn']); // Sensibel!
// Verwundbar: Dokumenten-Download
function download_document($doc_id) {
$conn = get_database_connection();
// Verwundbar: Keine Eigentumsverifizierung
$stmt = $conn->prepare("SELECT file_path, filename FROM documents WHERE doc_id = ?");
$stmt->bind_param("i", $doc_id);
$stmt->execute();
$doc = $stmt->get_result()->fetch_assoc();
// Angreifer kann jedes Dokument durch ID-Aufzählung herunterladen
header('Content-Disposition: attachment; filename="' . $doc['filename'] . '"');
readfile($doc['file_path']);
}
?>
// Verwundbar: Node.js API mit IDOR
const express = require('express');
const app = express();
// Verwundbar: Kontostand abrufen
app.get('/api/account/:accountId', async (req, res) => {
const accountId = req.params.accountId; // Benutzergesteuert
// Verwundbar: Keine Autorisierungsprüfung
const account = await db.query(
'SELECT * FROM accounts WHERE account_id = $1',
[accountId]
);
// Gibt jeden Kontostand zurück, nicht nur den des Benutzers
res.json(account.rows[0]);
});
// Verwundbar: Geld überweisen
app.post('/api/transfer', async (req, res) => {
const { fromAccountId, toAccountId, amount } = req.body;
// Verwundbar: Keine Verifizierung dass Benutzer fromAccountId besitzt
await db.query(
'UPDATE accounts SET balance = balance - $1 WHERE account_id = $2',
[amount, fromAccountId]
);
await db.query(
'UPDATE accounts SET balance = balance + $1 WHERE account_id = $2',
[amount, toAccountId]
);
// Angreifer kann von jedem Konto überweisen!
res.json({ success: true });
});
// Verwundbar: Nachricht löschen
app.delete('/api/messages/:messageId', async (req, res) => {
const messageId = req.params.messageId;
// Verwundbar: Löscht jede Nachricht unabhängig vom Eigentum
await db.query('DELETE FROM messages WHERE message_id = $1', [messageId]);
res.json({ deleted: true });
});
Lösungscode
// Behoben: C# mit Autorisierungsprüfung
using System.Data.SqlClient;
using System.Web.UI;
public partial class SecureInvoice : Page
{
protected void Page_Load(object sender, EventArgs e)
{
// Aktuelle Benutzer-ID aus authentifizierter Sitzung abrufen
int currentUserId = GetAuthenticatedUserId();
if (currentUserId == 0)
{
Response.Redirect("/login");
return;
}
string invoiceId = Request.QueryString["id"];
string connectionString = ConfigurationManager.ConnectionStrings["DB"].ConnectionString;
using (SqlConnection conn = new SqlConnection(connectionString))
{
// Behoben: Abfrage enthält Eigentumsprüfung
string query = @"SELECT * FROM invoices
WHERE invoice_id = @id
AND customer_id = @userId";
SqlCommand cmd = new SqlCommand(query, conn);
cmd.Parameters.AddWithValue("@id", invoiceId);
cmd.Parameters.AddWithValue("@userId", currentUserId); // Behoben: Eigentum verifizieren
conn.Open();
SqlDataReader reader = cmd.ExecuteReader();
if (reader.Read())
{
lblInvoiceNumber.Text = reader["invoice_number"].ToString();
lblAmount.Text = reader["amount"].ToString();
lblCustomerName.Text = reader["customer_name"].ToString();
}
else
{
// Behoben: Rechnung nicht gefunden ODER Benutzer besitzt sie nicht
lblError.Text = "Rechnung nicht gefunden";
}
}
}
private int GetAuthenticatedUserId()
{
// Benutzer-ID aus sicherer Sitzung abrufen
return Session["UserId"] != null ? (int)Session["UserId"] : 0;
}
}
// Behoben: Java Servlet mit ordnungsgemäßer Autorisierung
import javax.servlet.http.*;
import java.sql.*;
public class SecureOrderServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// Behoben: Authentifizierten Benutzer abrufen
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute("userId") == null) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
Long userId = (Long) session.getAttribute("userId");
String orderId = request.getParameter("orderId");
try (Connection conn = dataSource.getConnection()) {
// Behoben: Benutzer-ID in Abfrage einbeziehen um Eigentum zu verifizieren
String sql = "SELECT * FROM orders WHERE order_id = ? AND customer_id = ?";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setString(1, orderId);
stmt.setLong(2, userId);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
Order order = mapOrder(rs);
request.setAttribute("order", order);
request.getRequestDispatcher("/order.jsp").forward(request, response);
} else {
// Behoben: Bestellung nicht gefunden oder Benutzer besitzt sie nicht
response.sendError(HttpServletResponse.SC_NOT_FOUND, "Bestellung nicht gefunden");
}
} catch (SQLException e) {
throw new ServletException(e);
}
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute("userId") == null) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
Long userId = (Long) session.getAttribute("userId");
String orderId = request.getParameter("orderId");
String newStatus = request.getParameter("status");
// Behoben: Statuswert validieren
if (!isValidStatus(newStatus)) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Ungültiger Status");
return;
}
try (Connection conn = dataSource.getConnection()) {
// Behoben: customer_id in WHERE-Klausel einbeziehen
String sql = "UPDATE orders SET status = ? WHERE order_id = ? AND customer_id = ?";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setString(1, newStatus);
stmt.setString(2, orderId);
stmt.setLong(3, userId);
int updated = stmt.executeUpdate();
if (updated == 0) {
// Behoben: Bestellung nicht gefunden oder Benutzer besitzt sie nicht
response.sendError(HttpServletResponse.SC_NOT_FOUND, "Bestellung nicht gefunden");
} else {
response.setStatus(HttpServletResponse.SC_OK);
}
} catch (SQLException e) {
throw new ServletException(e);
}
}
}
<?php
// Behoben: PHP mit Autorisierungsprüfungen
function get_user_profile($profile_id, $current_user_id) {
$conn = get_database_connection();
// Behoben: Autorisierung verifizieren
// Option 1: Nur Benutzern erlauben ihr eigenes Profil zu sehen
if ($profile_id != $current_user_id && !is_admin($current_user_id)) {
return null; // Nicht autorisiert
}
$stmt = $conn->prepare("SELECT * FROM users WHERE user_id = ?");
$stmt->bind_param("i", $profile_id);
$stmt->execute();
return $stmt->get_result()->fetch_assoc();
}
// Behoben: Sicherer Profilzugriff
session_start();
$current_user_id = $_SESSION['user_id'] ?? null;
if (!$current_user_id) {
header('Location: /login');
exit;
}
$profile_id = $_GET['id'];
$profile = get_user_profile($profile_id, $current_user_id);
if (!$profile) {
http_response_code(403);
die('Zugriff verweigert');
}
// Behoben: Dokumenten-Download mit Eigentumsprüfung
function download_document($doc_id, $user_id) {
$conn = get_database_connection();
// Behoben: owner_id in Abfrage einbeziehen
$stmt = $conn->prepare(
"SELECT file_path, filename FROM documents
WHERE doc_id = ? AND owner_id = ?"
);
$stmt->bind_param("ii", $doc_id, $user_id);
$stmt->execute();
$doc = $stmt->get_result()->fetch_assoc();
if (!$doc) {
http_response_code(404);
die('Dokument nicht gefunden');
}
// Behoben: Dateipfad validieren liegt im erlaubten Verzeichnis
$real_path = realpath($doc['file_path']);
$allowed_dir = realpath('/var/www/documents/');
if (strpos($real_path, $allowed_dir) !== 0) {
http_response_code(403);
die('Zugriff verweigert');
}
header('Content-Disposition: attachment; filename="' .
basename($doc['filename']) . '"');
readfile($real_path);
}
?>
// Behoben: Node.js API mit ordnungsgemäßer Autorisierung
const express = require('express');
const app = express();
// Behoben: Authentifizierungs-Middleware
function authenticate(req, res, next) {
if (!req.session || !req.session.userId) {
return res.status(401).json({ error: 'Nicht autorisiert' });
}
next();
}
// Behoben: Kontostand mit Eigentumsprüfung abrufen
app.get('/api/account/:accountId', authenticate, async (req, res) => {
const accountId = req.params.accountId;
const userId = req.session.userId;
// Behoben: Eigentum in Abfrage verifizieren
const result = await db.query(
'SELECT * FROM accounts WHERE account_id = $1 AND user_id = $2',
[accountId, userId]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Konto nicht gefunden' });
}
res.json(result.rows[0]);
});
// Behoben: Geld überweisen mit Eigentumsverifizierung
app.post('/api/transfer', authenticate, async (req, res) => {
const { fromAccountId, toAccountId, amount } = req.body;
const userId = req.session.userId;
// Behoben: Verifizieren dass Benutzer das Quellkonto besitzt
const fromAccount = await db.query(
'SELECT * FROM accounts WHERE account_id = $1 AND user_id = $2',
[fromAccountId, userId]
);
if (fromAccount.rows.length === 0) {
return res.status(403).json({ error: 'Zugriff auf Quellkonto verweigert' });
}
// Behoben: Ausreichendes Guthaben verifizieren
if (fromAccount.rows[0].balance < amount) {
return res.status(400).json({ error: 'Unzureichendes Guthaben' });
}
// Behoben: Transaktion für atomare Überweisung verwenden
const client = await db.getClient();
try {
await client.query('BEGIN');
await client.query(
'UPDATE accounts SET balance = balance - $1 WHERE account_id = $2 AND user_id = $3',
[amount, fromAccountId, userId]
);
await client.query(
'UPDATE accounts SET balance = balance + $1 WHERE account_id = $2',
[amount, toAccountId]
);
await client.query('COMMIT');
res.json({ success: true });
} catch (err) {
await client.query('ROLLBACK');
throw err;
} finally {
client.release();
}
});
// Behoben: Nachricht löschen mit Eigentumsprüfung
app.delete('/api/messages/:messageId', authenticate, async (req, res) => {
const messageId = req.params.messageId;
const userId = req.session.userId;
// Behoben: owner_id beim Löschen einbeziehen
const result = await db.query(
'DELETE FROM messages WHERE message_id = $1 AND owner_id = $2 RETURNING *',
[messageId, userId]
);
if (result.rowCount === 0) {
return res.status(404).json({ error: 'Nachricht nicht gefunden' });
}
res.json({ deleted: true });
});
module.exports = app;
CVE-Beispiele
- CVE-2019-17382: Asterisk Manager Interface erlaubte nicht autorisierten Zugriff durch manipulierte ActionID.
- CVE-2018-7584: PHP IMAP-Funktion erlaubte IDOR durch Mailbox-Parameter-Manipulation.
- CVE-2020-13151: Aerospike erlaubte nicht autorisierten Datenzugriff durch vorhersagbare Datensatz-IDs.
Referenzen
- MITRE Corporation. "CWE-566: Authorization Bypass Through User-Controlled SQL Primary Key." https://cwe.mitre.org/data/definitions/566.html
- OWASP. "Insecure Direct Object References Prevention Cheat Sheet."
- OWASP. "Testing for Insecure Direct Object References."