Übermäßige Anzahl ineffizienter serverseitiger Datenzugriffe

Beschreibung

Übermäßige Anzahl ineffizienter serverseitiger Datenzugriffe tritt auf, wenn Software zu viele Datenabfragen durchführt, ohne effiziente Datenverarbeitungsfunktionalität wie Stored Procedures, Batch-Operationen oder optimierte Abfragemuster zu verwenden. CISQ empfiehlt einen Standardschwellenwert von maximal 5 Datenabfragen für eine ineffiziente Funktion oder Prozedur. Wenn Code zahlreiche einzelne Datenbankaufrufe macht, wo weniger, effizientere Aufrufe ausreichen würden, erzeugt dies Leistungsengpässe und erhöht die Latenz. Dies ist allgemein als "N+1 Query Problem" oder "geschwätziger" Datenzugriff bekannt.

Risiko

Obwohl hauptsächlich ein Leistungsproblem, haben übermäßige Datenzugriffe Sicherheitsimplikationen. Angreifer können langsame Endpunkte für Denial-of-Service-Angriffe ausnutzen, indem sie Pfade mit vielen Datenbankabfragen auslösen. Datenbankverbindungspool-Erschöpfung kann unter Last auftreten. Erhöhte Latenz kann Timeouts verursachen, die zu inkonsistenten Zuständen führen. Die zusätzlichen Round-Trips vergrößern das Fenster für Race Conditions. Hohe Datenbanklast kann die gesamte Systemverfügbarkeit beeinträchtigen. Geschwätziger Datenzugriffsmuster erhöhen auch die Angriffsfläche für SQL-Injection, wenn Abfragen nicht ordnungsgemäß parametrisiert sind.

Lösung

Verwenden Sie Batch-Operationen anstelle von einzelnen Abfragen in Schleifen. Implementieren Sie Eager Loading oder JOIN-Abfragen, um verwandte Daten in einzelnen Abfragen abzurufen. Verwenden Sie Stored Procedures für komplexe mehrstufige Operationen. Implementieren Sie Query-Ergebnis-Caching. Verwenden Sie Paginierung für große Ergebnismengen. Setzen Sie ORM-Features wie Eager Loading, Batch-Fetching und Query-Optimierung ein. Setzen Sie Datenbankabfragezählungslimits und Warnungen. Verwenden Sie Query-Profiling, um N+1-Probleme zu identifizieren. Erwägen Sie Denormalisierung für leseintensive Operationen.

Häufige Auswirkungen

AuswirkungDetails
VerfügbarkeitBereich: Verfügbarkeit

DoS: Ressourcenverbrauch - Mehrere ineffiziente Abfragen verbrauchen Datenbankressourcen und können Verbindungspools erschöpfen.
VerfügbarkeitBereich: Verfügbarkeit

Leistungsreduktion - Latenz erhöht sich signifikant mit jedem zusätzlichen Datenbank-Round-Trip.
AndereBereich: Ändere

Qualitätsverschlechterung - Ineffiziente Datenzugriffsmuster machen das System schwerer zu skalieren und zu warten.

Beispielcode

Anfälliger Code

// Anfällig: N+1 Query Problem - eine Abfrage pro Bestellposition
public class VulnerableOrderService {

    public OrderDetails getOrderDetails(Long orderId) {
        // Abfrage 1: Bestellung abrufen
        Order order = orderRepository.findById(orderId);

        // N Abfragen: Eine Abfrage pro Position für Produktdetails
        List<ItemDetails> itemDetails = new ArrayList<>();
        for (OrderItem item : order.getItems()) {
            // Anfällig: Abfrage in einer Schleife!
            Product product = productRepository.findById(item.getProductId());

            // Weitere Abfrage pro Position für Bestand
            Inventory inventory = inventoryRepository.findByProductId(item.getProductId());

            // Weitere Abfrage pro Position für Preise
            Pricing pricing = pricingRepository.findByProductId(item.getProductId());

            itemDetails.add(new ItemDetails(item, product, inventory, pricing));
        }

        // Bei 100 Positionen: 1 + (100 * 3) = 301 Abfragen!
        return new OrderDetails(order, itemDetails);
    }
}
# Anfällig: Mehrere Abfragen in Schleife
class VulnerableUserService:

    def get_users_with_details(self, user_ids):
        users = []

        for user_id in user_ids:
            # Abfrage 1: Benutzer abrufen
            user = self.db.query(User).get(user_id)

            # Abfrage 2: Profil abrufen (pro Benutzer)
            profile = self.db.query(Profile).filter(
                Profile.user_id == user_id
            ).first()

            # Abfrage 3: Rollen abrufen (pro Benutzer)
            roles = self.db.query(Role).join(UserRole).filter(
                UserRole.user_id == user_id
            ).all()

            # Abfrage 4: Einstellungen abrufen (pro Benutzer)
            preferences = self.db.query(Preference).filter(
                Preference.user_id == user_id
            ).all()

            users.append({
                'user': user,
                'profile': profile,
                'roles': roles,
                'preferences': preferences
            })

        # Bei 50 Benutzern: 50 * 4 = 200 Abfragen!
        return users

Korrigierter Code

// Korrigiert: Batch-Abfragen und Eager Loading
public class FixedOrderService {

    public OrderDetails getOrderDetails(Long orderId) {
        // Einzelne Abfrage mit JOINs um alle verwandten Daten abzurufen
        Order order = orderRepository.findByIdWithDetails(orderId);

        // Alle Daten in einer Abfrage via JPQL mit JOIN FETCH geladen
        return mapToOrderDetails(order);
    }

    // Batch-Fetch für mehrere Bestellungen verwenden
    public List<OrderDetails> getMultipleOrderDetails(List<Long> orderIds) {
        // Einzelne Abfrage für alle Bestellungen mit allen Beziehungen
        List<Order> orders = orderRepository.findAllByIdWithDetails(orderIds);
        return orders.stream().map(this::mapToOrderDetails).collect(toList());
    }
}

@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {

    // Korrigiert: Alle Beziehungen in einzelner Abfrage eager laden
    @Query("SELECT DISTINCT o FROM Order o " +
           "JOIN FETCH o.items i " +
           "JOIN FETCH i.product p " +
           "JOIN FETCH p.inventory " +
           "JOIN FETCH p.pricing " +
           "WHERE o.id = :orderId")
    Order findByIdWithDetails(@Param("orderId") Long orderId);

    // Korrigiert: Batch-Fetch für mehrere Bestellungen
    @Query("SELECT DISTINCT o FROM Order o " +
           "JOIN FETCH o.items i " +
           "JOIN FETCH i.product p " +
           "WHERE o.id IN :orderIds")
    List<Order> findAllByIdWithDetails(@Param("orderIds") List<Long> orderIds);
}
# Korrigiert: Eager Loading mit SQLAlchemy
from sqlalchemy.orm import joinedload, selectinload

class FixedUserService:

    def get_users_with_details(self, user_ids):
        # Korrigiert: Einzelne Abfrage mit Eager Loading
        users = self.db.query(User)\
            .options(
                joinedload(User.profile),           # Profil eager laden
                selectinload(User.roles),           # Rollen batch laden
                selectinload(User.preferences)      # Einstellungen batch laden
            )\
            .filter(User.id.in_(user_ids))\
            .all()

        # Gibt alle Daten mit nur 1-4 Abfragen insgesamt zurück
        # (Hauptabfrage + eine für jedes selectinload)
        return [{
            'user': user,
            'profile': user.profile,
            'roles': user.roles,
            'preferences': user.preferences
        } for user in users]


# Alternative: Rohes SQL mit JOINs verwenden
class FixedUserServiceSQL:

    def get_users_with_details(self, user_ids):
        # Einzelne Abfrage mit JOINs
        query = """
            SELECT u.*, p.*, r.name as role_name
            FROM users u
            LEFT JOIN profiles p ON p.user_id = u.id
            LEFT JOIN user_roles ur ON ur.user_id = u.id
            LEFT JOIN roles r ON r.id = ur.role_id
            WHERE u.id = ANY(:user_ids)
        """
        results = self.db.execute(query, {'user_ids': user_ids}).fetchall()
        return self._group_results(results)
// Korrigiert: Batch-Abfragen mit JOINs
async function fixedGetOrdersWithCustomers(orderIds) {
    if (orderIds.length === 0) return [];

    // Korrigiert: Einzelne Abfrage mit JOINs
    const results = await db.query(`
        SELECT
            o.*,
            c.id as customer_id, c.name as customer_name, c.email,
            a.id as address_id, a.street, a.city, a.country
        FROM orders o
        JOIN customers c ON c.id = o.customer_id
        LEFT JOIN addresses a ON a.id = o.shipping_address_id
        WHERE o.id = ANY($1)
    `, [orderIds]);

    // Korrigiert: Positionen für alle Bestellungen auf einmal batch-fetchen
    const allItems = await db.query(`
        SELECT * FROM order_items
        WHERE order_id = ANY($1)
    `, [orderIds]);

    // Positionen nach order_id in Anwendung gruppieren
    const itemsByOrder = allItems.reduce((acc, item) => {
        if (!acc[item.order_id]) acc[item.order_id] = [];
        acc[item.order_id].push(item);
        return acc;
    }, {});

    // Ergebnisse kombinieren
    return results.map(row => ({
        order: extractOrder(row),
        customer: extractCustomer(row),
        address: extractAddress(row),
        items: itemsByOrder[row.id] || []
    }));

    // Jetzt nur 2 Abfragen insgesamt statt 400!
}

CVE-Beispiele

Diese CWE ist für direkte CVE-Zuordnung als VERBOTEN markiert, da sie ein Leistungs-/Qualitätsproblem und keine direkte Sicherheitsschwachstelle darstellt.


Verwandte CWEs

  • CWE-1120: Excessive Code Complexity (Eltern)
  • CWE-1226: Complexity Issues (Kategoriemitglied)
  • CWE-400: Uncontrolled Resource Consumption (kann führen zu)

Referenzen

  1. MITRE Corporation. "CWE-1060: Excessive Number of Inefficient Server-Side Data Accesses." https://cwe.mitre.org/data/definitions/1060.html

  2. CISQ. "Automated Source Code Quality Measures."

  3. Fowler, Martin. "OrmHate" - discussing N+1 query problems.