Übermäßiger Index-Bereichsscan für eine Datenressource

Beschreibung

Übermäßiger Index-Bereichsscan für eine Datenressource tritt auf, wenn ein Produkt einen Index-Bereichsscan auf einer großen Datentabelle durchführt, der eine große Anzahl von Zeilen abdecken kann. Laut CISQ-Empfehlungen gelten Tabellen mit mehr als 1.000.000 Zeilen als "groß", und ein Index-Bereichsscan, der 10 oder mehr Zeilen abdeckt, gilt als problematisch. Während Index-Scans schneller sind als vollständige Tabellen-Scans, können schlecht entworfene Abfragen, die große Teile eines Index scannen, immer noch erhebliche Leistungsprobleme verursachen, besonders wenn der Scan dann viele Zeilen aus der Haupttabelle abrufen muss.

Risiko

Übermäßige Index-Bereichsscans haben Sicherheitsimplikationen. Langsame Abfragen können für Denial-of-Service-Angriffe ausgenutzt werden. Ressourcenintensive Scans erschöpfen Datenbank-CPU und I/O-Kapazität. Lock-Contention während langlebiger Scans kann andere Operationen blockieren. Angreifer können Eingaben erstellen, die Worst-Case-Abfrageleistung auslösen. Speicherverbrauch steigt bei Verarbeitung größer Ergebnismengen. Abfrage-Timeouts können Anwendungsausfälle verursachen. Die vorhersagbare Leistungsverschlechterung ermöglicht Verstärkungsangriffe.

Lösung

Verwenden Sie geeignete Indizes, die Punkt-Lookups anstelle von Bereichsscans unterstützen, wenn möglich. Fügen Sie zusätzliche Filterkriterien hinzu, um Index-Bereiche einzugrenzen. Verwenden Sie Covering-Indizes, um Tabellen-Lookups für indizierte Daten zu vermeiden. Erwägen Sie Abfrageumschreibungen, um die Selektivität zu verbessern. Implementieren Sie Abfrageergebnis-Limits, um unbegrenzte Scans zu verhindern. Verwenden Sie Datenbank-Abfrageanalyse-Tools, um teure Index-Scans zu identifizieren. Wenden Sie Datenbankpartitionierung an, um den Scan-Bereich zu reduzieren. Cachen Sie häufig abgerufene Abfrageergebnisse. Implementieren Sie Abfrage-Timeouts, um unkontrollierte Scans zu verhindern. Überwachen Sie Abfrageleistung und optimieren Sie langsame Abfragen proaktiv.

Häufige Auswirkungen

AuswirkungDetails
VerfügbarkeitBereich: Verfügbarkeit

DoS: Ressourcenverbrauch - Größe Index-Scans verbrauchen erhebliche CPU-, I/O- und Speicherressourcen.
VerfügbarkeitBereich: Verfügbarkeit

Reduzierte Leistung - Abfragen dauern übermäßig lange, was die Benutzererfahrung beeinträchtigt.
AndereBereich: Ändere

Lock-Contention - Langlebige Scans können Sperren halten, die andere Operationen blockieren.

Beispielcode

Anfälliger Code

-- Anfällig: Index-Bereichsscan deckt zu viele Zeilen ab
-- Tabelle hat 10 Millionen Zeilen mit Index auf created_at

-- Anfällig: Datumsbereich deckt 6 Monate Daten ab
SELECT * FROM orders
WHERE created_at >= '2024-01-01'
  AND created_at < '2024-07-01';
-- Index-Scan könnte 5 Millionen Zeilen abdecken!

-- Anfällig: Führendes Wildcard erzwingt Scan des gesamten Index
SELECT * FROM customers
WHERE email LIKE '%@gmail.com';
-- Kann Index nicht effizient nutzen - scannt alle Zeilen

-- Anfällig: Spalte mit niedriger Kardinalität im Bereich
SELECT * FROM users
WHERE status = 'active'
  AND country IN ('US', 'CA', 'UK', 'AU', 'DE');
-- Wenn 90% der Benutzer 'active' sind, scannt dies die meiste Tabelle

-- Anfällig: OR-Bedingungen verhindern Index-Optimierung
SELECT * FROM products
WHERE category_id = 5
   OR price BETWEEN 10 AND 1000
   OR name LIKE 'Widget%';
-- Kann zu mehreren Index-Scans oder vollem Tabellen-Scan führen

-- Anfällig: Funktion auf indizierter Spalte
SELECT * FROM events
WHERE YEAR(event_date) = 2024
  AND MONTH(event_date) = 6;
-- Funktion verhindert direkte Index-Nutzung, erzwingt vollen Scan
// Anfällig: JPA-Abfrage mit übermäßigem Bereichsscan
@Repository
public class VulnerableOrderRepository {

    @PersistenceContext
    private EntityManager em;

    // Anfällig: Unbegrenzter Datumsbereich-Abfrage
    public List<Order> findOrdersByDateRange(LocalDate start, LocalDate end) {
        // Dies könnte Millionen von Zeilen zurückgeben!
        return em.createQuery(
            "SELECT o FROM Order o WHERE o.createdAt BETWEEN :start AND :end",
            Order.class)
            .setParameter("start", start)
            .setParameter("end", end)
            .getResultList();  // Kein Limit!
    }

    // Anfällig: Abfrage mit niedriger Selektivität
    public List<Order> findPendingOrders() {
        // Wenn die meisten Bestellungen 'PENDING' sind, scannt dies die meiste Tabelle
        return em.createQuery(
            "SELECT o FROM Order o WHERE o.status = 'PENDING'",
            Order.class)
            .getResultList();
    }

    // Anfällig: Textsuche ohne Volltext-Index
    public List<Product> searchProducts(String searchTerm) {
        // LIKE mit führendem Wildcard - immer voller Scan
        return em.createQuery(
            "SELECT p FROM Product p WHERE p.description LIKE :term",
            Product.class)
            .setParameter("term", "%" + searchTerm + "%")
            .getResultList();
    }
}
# Anfällig: Django ORM mit ineffizienten Abfragen
from django.db import models
from datetime import datetime, timedelta


class VulnerableAnalyticsService:

    def get_user_activity(self, start_date, end_date):
        # Anfällig: Könnte Millionen von Datensätzen zurückgeben
        # Index auf timestamp, aber Bereich ist zu breit
        return UserActivity.objects.filter(
            timestamp__gte=start_date,
            timestamp__lt=end_date
        )  # Kein Limit, keine Paginierung

    def search_logs(self, search_term):
        # Anfällig: Groß-/Kleinschreibung-unabhängiges LIKE mit Wildcards
        # Erzwingt vollen Tabellen-Scan unabhängig von Indizes
        return LogEntry.objects.filter(
            message__icontains=search_term
        )

    def get_orders_by_status(self, statuses):
        # Anfällig: IN-Klausel mit vielen Werten auf Spalte niedriger Kardinalität
        # Könnte große Teile der Tabelle scannen
        return Order.objects.filter(
            status__in=statuses,
            is_deleted=False
        ).order_by('-created_at')  # Immer noch kein Limit!

    def get_recent_high_value_orders(self):
        # Anfällig: OR-Bedingungen verhindern effiziente Index-Nutzung
        return Order.objects.filter(
            models.Q(total__gte=1000) |
            models.Q(is_priority=True) |
            models.Q(customer__is_vip=True)
        ).filter(
            created_at__gte=datetime.now() - timedelta(days=90)
        )

Korrigierter Code

-- Korrigiert: Optimierte Abfragen mit kontrollierten Index-Scans

-- Korrigiert: Enger Datumsbereich mit Paginierung
SELECT * FROM orders
WHERE created_at >= '2024-06-01'
  AND created_at < '2024-06-08'  -- Nur eine Woche
ORDER BY created_at
LIMIT 100 OFFSET 0;  -- Paginierung

-- Korrigiert: Covering-Index für E-Mail-Domain-Lookup
-- Index erstellen: CREATE INDEX idx_email_domain ON customers(email_domain);
-- Spalte hinzufügen: ALTER TABLE customers ADD email_domain VARCHAR(100)
--             GENERATED ALWAYS AS (SUBSTRING_INDEX(email, '@', -1));
SELECT * FROM customers
WHERE email_domain = 'gmail.com'
LIMIT 100;

-- Korrigiert: Zusammengesetzter Index mit Spalte hoher Kardinalität zuerst
-- Index erstellen: CREATE INDEX idx_country_status ON users(country, status);
SELECT * FROM users
WHERE country = 'US'
  AND status = 'active'
LIMIT 100;

-- Korrigiert: UNION ALL statt OR für separate Index-Nutzung
SELECT * FROM products WHERE category_id = 5 LIMIT 100
UNION ALL
SELECT * FROM products WHERE price BETWEEN 10 AND 100 LIMIT 100
UNION ALL
SELECT * FROM products WHERE name LIKE 'Widget%' LIMIT 100;

-- Korrigiert: Datumsbereich verwenden, der Index-Nutzung ermöglicht
SELECT * FROM events
WHERE event_date >= '2024-06-01'
  AND event_date < '2024-07-01'
LIMIT 100;

-- Korrigiert: Partitionierte Tabelle für datumsbasierte Abfragen
-- Tabelle nach Monat partitioniert - Abfrage scannt nur relevante Partition
SELECT * FROM orders_partitioned
WHERE created_at >= '2024-06-01'
  AND created_at < '2024-07-01'
LIMIT 100;
// Korrigiert: JPA-Repository mit optimierten Abfragen
@Repository
public class FixedOrderRepository {

    @PersistenceContext
    private EntityManager em;

    // Korrigiert: Paginierte Ergebnisse mit vernünftiger Seitengröße
    public Page<Order> findOrdersByDateRange(
            LocalDate start, LocalDate end, Pageable pageable) {

        // Maximalen Datumsbereich erzwingen
        if (ChronoUnit.DAYS.between(start, end) > 30) {
            throw new IllegalArgumentException(
                "Datumsbereich kann 30 Tage nicht überschreiten");
        }

        String countQuery = "SELECT COUNT(o) FROM Order o " +
                           "WHERE o.createdAt BETWEEN :start AND :end";

        String dataQuery = "SELECT o FROM Order o " +
                          "WHERE o.createdAt BETWEEN :start AND :end " +
                          "ORDER BY o.createdAt DESC";

        Long count = em.createQuery(countQuery, Long.class)
            .setParameter("start", start)
            .setParameter("end", end)
            .getSingleResult();

        List<Order> orders = em.createQuery(dataQuery, Order.class)
            .setParameter("start", start)
            .setParameter("end", end)
            .setFirstResult((int) pageable.getOffset())
            .setMaxResults(pageable.getPageSize())
            .getResultList();

        return new PageImpl<>(orders, pageable, count);
    }

    // Korrigiert: Zusammengesetzten Index verwenden und Ergebnisse begrenzen
    public List<Order> findRecentPendingOrders(int limit) {
        // Index: (status, created_at DESC)
        // Fügt Zeitbeschränkung hinzu, um Scan zu begrenzen
        LocalDate cutoff = LocalDate.now().minusDays(7);

        return em.createQuery(
            "SELECT o FROM Order o " +
            "WHERE o.status = 'PENDING' " +
            "AND o.createdAt >= :cutoff " +
            "ORDER BY o.createdAt DESC",
            Order.class)
            .setParameter("cutoff", cutoff)
            .setMaxResults(limit)
            .getResultList();
    }

    // Korrigiert: Volltext-Suche anstelle von LIKE verwenden
    public List<Product> searchProducts(String searchTerm, int limit) {
        // Datenbank-Volltext-Suche verwenden (MySQL-Beispiel)
        return em.createNativeQuery(
            "SELECT * FROM products " +
            "WHERE MATCH(name, description) AGAINST(:term IN NATURAL LANGUAGE MODE) " +
            "LIMIT :limit",
            Product.class)
            .setParameter("term", searchTerm)
            .setParameter("limit", limit)
            .getResultList();
    }
}
# Korrigiert: Django ORM mit optimierten Abfragen
from django.db import models
from django.core.paginator import Paginator
from datetime import datetime, timedelta
from django.contrib.postgres.search import SearchVector, SearchQuery


class FixedAnalyticsService:

    MAX_DAYS_RANGE = 7
    DEFAULT_PAGE_SIZE = 100

    def get_user_activity(self, start_date, end_date, page=1):
        # Korrigiert: Maximalen Datumsbereich erzwingen
        if (end_date - start_date).days > self.MAX_DAYS_RANGE:
            raise ValueError(
                f"Datumsbereich kann {self.MAX_DAYS_RANGE} Tage nicht überschreiten"
            )

        # Korrigiert: Paginierte Ergebnisse
        queryset = UserActivity.objects.filter(
            timestamp__gte=start_date,
            timestamp__lt=end_date
        ).order_by('-timestamp')

        paginator = Paginator(queryset, self.DEFAULT_PAGE_SIZE)
        return paginator.get_page(page)

    def search_logs(self, search_term, limit=100):
        # Korrigiert: PostgreSQL-Volltext-Suche verwenden
        # Erfordert: CREATE INDEX idx_log_search ON log_entry
        #           USING gin(to_tsvector('english', message));
        search_query = SearchQuery(search_term)

        return LogEntry.objects.annotate(
            search=SearchVector('message')
        ).filter(
            search=search_query
        ).order_by('-timestamp')[:limit]

    def get_orders_by_status(self, statuses, days_back=7, limit=100):
        # Korrigiert: Zeitbeschränkung und Limit hinzufügen
        cutoff = datetime.now() - timedelta(days=days_back)

        return Order.objects.filter(
            status__in=statuses,
            is_deleted=False,
            created_at__gte=cutoff  # Bereich eingrenzen
        ).order_by('-created_at')[:limit]

    def get_recent_high_value_orders(self, limit=100):
        # Korrigiert: Separate Abfragen mit UNION für bessere Index-Nutzung
        # Oder eine berechnete/denormalisierte Spalte verwenden

        # Option 1: Eine 'requires_attention' Boolean-Spalte hinzufügen
        # die durch Trigger/Signals aktualisiert wird
        return Order.objects.filter(
            requires_attention=True,
            created_at__gte=datetime.now() - timedelta(days=7)
        ).order_by('-created_at')[:limit]

    def get_orders_with_cursor_pagination(self, cursor=None, page_size=100):
        """
        Korrigiert: Cursor-basierte Paginierung für große Datensätze.
        Effizienter als Offset-Paginierung für große Offsets.
        """
        queryset = Order.objects.order_by('-created_at', '-id')

        if cursor:
            # cursor ist (created_at, id) Tuple
            created_at, order_id = cursor
            queryset = queryset.filter(
                models.Q(created_at__lt=created_at) |
                models.Q(created_at=created_at, id__lt=order_id)
            )

        orders = list(queryset[:page_size + 1])

        has_next = len(orders) > page_size
        if has_next:
            orders = orders[:page_size]

        next_cursor = None
        if has_next and orders:
            last = orders[-1]
            next_cursor = (last.created_at, last.id)

        return {
            'orders': orders,
            'next_cursor': next_cursor,
            'has_next': has_next
        }

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-405: Asymmetric Resource Consumption (Amplification) (Eltern)
  • CWE-1067: Excessive Execution of Sequential Searches of Data Resource (verwandt)
  • CWE-400: Uncontrolled Resource Consumption (kann führen zu)

Referenzen

  1. MITRE Corporation. "CWE-1094: Excessive Index Range Scan for a Data Resource." https://cwe.mitre.org/data/definitions/1094.html
  2. OMG ASCPEM-PRF-7. "Automated Source Code Performance Efficiency Measure."
  3. Use The Index, Luke. "Indexing for Range Queries." https://use-the-index-luke.com/