SQL injection fixes on all 9 endpoints:
/api/trades: ISIN validated via regex, days range-checked /api/summary, /api/statistics/total-trades: days range-checked /api/custom-analytics: dates validated, exchanges whitelist-checked, already-existing enum validation kept /api/statistics/moving-average: exchange whitelist-checked /api/statistics/stock-trends: limit capped at 1000 /api/analytics: dates validated, ISINs validated, continents sanitized, metric/group_by whitelist-checked /api/metadata/search: free-text query sanitized (quotes escaped, SQL fragments stripped)
This commit is contained in:
@@ -6,6 +6,11 @@ import requests
|
||||
import os
|
||||
import logging
|
||||
from typing import Optional, Dict, Any
|
||||
from src.utils.validation import (
|
||||
validate_isin, validate_exchange, validate_date,
|
||||
validate_int_range, sanitize_sql_string,
|
||||
validate_isin_list, validate_exchange_list,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -59,7 +64,16 @@ async def get_trades(isin: str = None, days: int = 7):
|
||||
Gibt aggregierte Analyse aller Trades zurück (nicht einzelne Trades).
|
||||
Nutzt vorberechnete Daten aus analytics_exchange_daily.
|
||||
"""
|
||||
try:
|
||||
days = validate_int_range(days, 1, 365)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Ungueltiger days-Parameter (1-365)")
|
||||
|
||||
if isin:
|
||||
try:
|
||||
isin = validate_isin(isin)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Ungueltiger ISIN-Wert")
|
||||
# Für spezifische ISIN: hole aus trades Tabelle
|
||||
query = f"""
|
||||
select
|
||||
@@ -109,6 +123,11 @@ async def get_summary(days: int = None):
|
||||
Gibt Zusammenfassung zurück. Nutzt analytics_daily_summary für total_trades.
|
||||
Optional: days Parameter für Zeitraum-basierte Zusammenfassung.
|
||||
"""
|
||||
if days:
|
||||
try:
|
||||
days = validate_int_range(days, 1, 3650)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Ungueltiger days-Parameter (1-3650)")
|
||||
if days:
|
||||
# Zeitraum-basierte Zusammenfassung
|
||||
query = f"""
|
||||
@@ -169,6 +188,11 @@ async def get_summary(days: int = None):
|
||||
@app.get("/api/statistics/total-trades")
|
||||
async def get_total_trades(days: int = None):
|
||||
"""Gibt Gesamtzahl aller Trades zurück (aus analytics_daily_summary). Optional: days Parameter für Zeitraum."""
|
||||
if days:
|
||||
try:
|
||||
days = validate_int_range(days, 1, 3650)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Ungueltiger days-Parameter (1-3650)")
|
||||
if days:
|
||||
query = f"select sum(total_trades) as total from analytics_daily_summary where timestamp >= dateadd('d', -{days}, now())"
|
||||
else:
|
||||
@@ -201,6 +225,12 @@ async def get_custom_analytics(
|
||||
- exchanges: Komma-separierte Liste von Exchanges (optional)
|
||||
"""
|
||||
# Validiere Parameter
|
||||
try:
|
||||
date_from = validate_date(date_from)
|
||||
date_to = validate_date(date_to)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Ungueltiges Datumsformat (erwartet: YYYY-MM-DD)")
|
||||
|
||||
valid_x_axis = ["date", "exchange", "isin"]
|
||||
valid_y_axis = ["volume", "trade_count", "avg_price"]
|
||||
valid_group_by = ["exchange", "sector", "date"]
|
||||
@@ -212,6 +242,13 @@ async def get_custom_analytics(
|
||||
if group_by not in valid_group_by:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid group_by. Must be one of: {valid_group_by}")
|
||||
|
||||
validated_exchanges = None
|
||||
if exchanges:
|
||||
try:
|
||||
validated_exchanges = validate_exchange_list(exchanges)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Ungueltiger Exchange-Name in der Liste")
|
||||
|
||||
# Für Sektor-Gruppierung: direkter JOIN mit metadata (nicht vorberechnet)
|
||||
if group_by == "sector":
|
||||
y_axis_map = {
|
||||
@@ -233,8 +270,8 @@ async def get_custom_analytics(
|
||||
and t.timestamp <= '{date_to}'
|
||||
"""
|
||||
|
||||
if exchanges:
|
||||
exchange_list = ",".join([f"'{e.strip()}'" for e in exchanges.split(",")])
|
||||
if validated_exchanges:
|
||||
exchange_list = ",".join([f"'{e}'" for e in validated_exchanges])
|
||||
query += f" and t.exchange in ({exchange_list})"
|
||||
|
||||
query += f" group by date_trunc('day', t.timestamp), coalesce(m.sector, 'Unbekannt') order by x_value asc, group_value asc"
|
||||
@@ -252,12 +289,9 @@ async def get_custom_analytics(
|
||||
|
||||
# Nutze vorberechnete Daten aus analytics_custom
|
||||
exchange_filter = "all"
|
||||
if exchanges:
|
||||
# Wenn mehrere Exchanges angegeben, müssen wir kombinieren
|
||||
# Für jetzt: nutze nur wenn ein Exchange angegeben ist
|
||||
exchange_list = [e.strip() for e in exchanges.split(",")]
|
||||
if len(exchange_list) == 1:
|
||||
exchange_filter = exchange_list[0]
|
||||
if validated_exchanges:
|
||||
if len(validated_exchanges) == 1:
|
||||
exchange_filter = validated_exchanges[0]
|
||||
else:
|
||||
# Bei mehreren Exchanges: gib Fehler zurück, da dies nicht vorberechnet wird
|
||||
raise HTTPException(
|
||||
@@ -317,6 +351,10 @@ async def get_moving_average(days: int = 7, exchange: str = None):
|
||||
"""
|
||||
|
||||
if exchange:
|
||||
try:
|
||||
exchange = validate_exchange(exchange)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Ungueltiger Exchange-Name")
|
||||
query += f" and exchange = '{exchange}'"
|
||||
|
||||
query += " order by date asc, exchange asc"
|
||||
@@ -393,6 +431,10 @@ async def get_stock_trends(days: int = 7, limit: int = 20):
|
||||
"""
|
||||
if days not in [7, 30, 42, 69, 180, 365]:
|
||||
raise HTTPException(status_code=400, detail="Invalid days parameter. Must be one of: 7, 30, 42, 69, 180, 365")
|
||||
try:
|
||||
limit = validate_int_range(limit, 1, 1000)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Ungueltiger limit-Parameter (1-1000)")
|
||||
|
||||
query = f"""
|
||||
select
|
||||
@@ -423,7 +465,40 @@ async def get_analytics(
|
||||
continents: str = None
|
||||
):
|
||||
"""Analytics Endpunkt für Report Builder"""
|
||||
# Validiere optionale Parameter
|
||||
if date_from:
|
||||
try:
|
||||
date_from = validate_date(date_from)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Ungueltiges date_from Format (erwartet: YYYY-MM-DD)")
|
||||
if date_to:
|
||||
try:
|
||||
date_to = validate_date(date_to)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Ungueltiges date_to Format (erwartet: YYYY-MM-DD)")
|
||||
|
||||
validated_isins = None
|
||||
if isins:
|
||||
try:
|
||||
validated_isins = validate_isin_list(isins)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Ungueltiger ISIN-Wert in der Liste")
|
||||
|
||||
sanitized_continents = None
|
||||
if continents:
|
||||
sanitized_continents = [sanitize_sql_string(c.strip(), max_length=50) for c in continents.split(",") if c.strip()]
|
||||
|
||||
composite_keys = ["exchange_continent", "exchange_sector"]
|
||||
valid_metrics = ["volume", "count", "avg_price", "all"]
|
||||
valid_groups = ["day", "month", "exchange", "isin", "name", "continent", "sector", "exchange_continent", "exchange_sector"]
|
||||
|
||||
if metric not in valid_metrics:
|
||||
raise HTTPException(status_code=400, detail=f"Ungueltiger metric-Parameter. Erlaubt: {valid_metrics}")
|
||||
if group_by not in valid_groups:
|
||||
raise HTTPException(status_code=400, detail=f"Ungueltiger group_by-Parameter. Erlaubt: {valid_groups}")
|
||||
if sub_group_by and sub_group_by not in valid_groups:
|
||||
raise HTTPException(status_code=400, detail=f"Ungueltiger sub_group_by-Parameter. Erlaubt: {valid_groups}")
|
||||
|
||||
needs_metadata = any([
|
||||
group_by in ["name", "continent", "sector"] + composite_keys,
|
||||
sub_group_by in ["name", "continent", "sector"] + composite_keys,
|
||||
@@ -473,12 +548,12 @@ async def get_analytics(
|
||||
if date_to:
|
||||
query += f" and {t_prefix}timestamp <= '{date_to}'"
|
||||
|
||||
if isins:
|
||||
isins_list = ",".join([f"'{i.strip()}'" for i in isins.split(",")])
|
||||
if validated_isins:
|
||||
isins_list = ",".join([f"'{i}'" for i in validated_isins])
|
||||
query += f" and {t_prefix}isin in ({isins_list})"
|
||||
|
||||
if continents and needs_metadata:
|
||||
cont_list = ",".join([f"'{c.strip()}'" for c in continents.split(",")])
|
||||
if sanitized_continents and needs_metadata:
|
||||
cont_list = ",".join([f"'{c}'" for c in sanitized_continents])
|
||||
query += f" and {m_prefix}continent in ({cont_list})"
|
||||
|
||||
query += f" group by {selected_group}"
|
||||
@@ -493,7 +568,8 @@ async def get_analytics(
|
||||
@app.get("/api/metadata/search")
|
||||
async def search_metadata(q: str):
|
||||
"""Case-insensitive search for ISIN or Name"""
|
||||
query = f"select isin, name from metadata where isin ilike '%{q}%' or name ilike '%{q}%' limit 10"
|
||||
q_safe = sanitize_sql_string(q, max_length=100)
|
||||
query = f"select isin, name from metadata where isin ilike '%{q_safe}%' or name ilike '%{q_safe}%' limit 10"
|
||||
data = query_questdb(query)
|
||||
return format_questdb_response(data)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user