Ü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
| Auswirkung | Details |
|---|---|
| Verfügbarkeit | Bereich: Verfügbarkeit DoS: Ressourcenverbrauch - Größe Index-Scans verbrauchen erhebliche CPU-, I/O- und Speicherressourcen. |
| Verfügbarkeit | Bereich: Verfügbarkeit Reduzierte Leistung - Abfragen dauern übermäßig lange, was die Benutzererfahrung beeinträchtigt. |
| Andere | Bereich: Ä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
- MITRE Corporation. "CWE-1094: Excessive Index Range Scan for a Data Resource." https://cwe.mitre.org/data/definitions/1094.html
- OMG ASCPEM-PRF-7. "Automated Source Code Performance Efficiency Measure."
- Use The Index, Luke. "Indexing for Range Queries." https://use-the-index-luke.com/