Session 2026-04-23: Security, Content-Schutz, Wiki-Temperament-Migration
Security (9 Fixes): - JWT_SECRET Pflicht-Check beim Start (Production) - Rate-Limit: Login (10/5min), Register (5/h), KI-Training (10/h), Giftköder (3/h) - KI-Training-Endpoint: Auth-Pflicht hinzugefügt - Private Profile aus Freunde-Suche gefiltert - OG-Tags XSS mit html.escape() gesichert - Globales File-Upload-Limit 20 MB (Middleware) - E-Mail-Maskierung für Moderatoren im Admin-Panel - IP-Blocklist in ratelimit.py Content-Schutz (4 Schichten): - robots.txt: /api/ komplett Disallow, SSR-Seiten Allow - Rate-Limit auf /api/wiki/rassen (60/min) + Detail (30/min) - Honeypot /api/wiki/trap + unsichtbarer Link in index.html - Wasserzeichen in KI-Enricher-Prompt Wiki Temperament-Migration: - 60-Wort Übersetzungsmap EN→DE - Datenmüll-Filter (hunderasse, dog breed etc.) - translate_existing_temperaments() + Admin-Button - SW by-v318, APP_VER 306
This commit is contained in:
parent
0f5f1c4c30
commit
15f854d96c
15 changed files with 284 additions and 53 deletions
|
|
@ -206,8 +206,11 @@ async def list_users(
|
|||
where += " AND u.rolle = ?"
|
||||
params.append(rolle)
|
||||
|
||||
# E-Mail nur für Admins; Moderatoren sehen maskierte Version
|
||||
_email_col = "u.email" if user["rolle"] == "admin" else \
|
||||
"SUBSTR(u.email,1,2)||'***@'||SUBSTR(u.email,INSTR(u.email,'@')+1) AS email"
|
||||
rows = conn.execute(f"""
|
||||
SELECT u.id, u.name, u.email, u.rolle, u.is_premium,
|
||||
SELECT u.id, u.name, {_email_col}, u.rolle, u.is_premium,
|
||||
u.is_moderator, u.is_banned, u.ban_reason,
|
||||
u.created_at, u.last_login,
|
||||
(SELECT COUNT(*) FROM dogs d WHERE d.user_id=u.id) AS dog_count,
|
||||
|
|
@ -587,6 +590,16 @@ async def wiki_enrich(data: WikiEnrichBody, user=Depends(require_mod)):
|
|||
return {"enriched": enriched, "remaining": remaining}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# POST /api/admin/wiki/translate-temperament — einmalige Migration
|
||||
# ------------------------------------------------------------------
|
||||
@router.post("/wiki/translate-temperament")
|
||||
async def wiki_translate_temperament(user=Depends(require_mod)):
|
||||
from scraper.breed_enricher import translate_existing_temperaments
|
||||
updated = translate_existing_temperaments()
|
||||
return {"updated": updated}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# DELETE /api/admin/wiki/zuchter/{id} — Züchter-Eintrag löschen (Admin/Mod)
|
||||
# ------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import secrets
|
|||
import string
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Response, Depends
|
||||
from fastapi import APIRouter, HTTPException, Request, Response, Depends
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from database import db
|
||||
from auth import (
|
||||
|
|
@ -13,6 +13,7 @@ from auth import (
|
|||
get_current_user
|
||||
)
|
||||
from username_blocklist import is_username_blocked
|
||||
from ratelimit import check as rl_check
|
||||
|
||||
router = APIRouter()
|
||||
COOKIE_NAME = "by_token"
|
||||
|
|
@ -43,7 +44,8 @@ def _set_cookie(response: Response, token: str):
|
|||
|
||||
|
||||
@router.post("/register")
|
||||
async def register(data: RegisterRequest, response: Response):
|
||||
async def register(data: RegisterRequest, response: Response, request: Request):
|
||||
rl_check(request, max_requests=5, window_seconds=3600, key="register")
|
||||
name = data.name.strip()
|
||||
if len(name) < 2:
|
||||
raise HTTPException(400, "Benutzername muss mindestens 2 Zeichen lang sein.")
|
||||
|
|
@ -90,7 +92,8 @@ async def register(data: RegisterRequest, response: Response):
|
|||
|
||||
|
||||
@router.post("/login")
|
||||
async def login(data: LoginRequest, response: Response):
|
||||
async def login(data: LoginRequest, response: Response, request: Request):
|
||||
rl_check(request, max_requests=10, window_seconds=300, key="login")
|
||||
with db() as conn:
|
||||
user = conn.execute(
|
||||
"SELECT id, pw_hash, name, rolle, is_premium FROM users WHERE email=?",
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ async def search_users(q: str = "", user=Depends(get_current_user)):
|
|||
FROM users u
|
||||
WHERE u.id != ?
|
||||
AND norm(u.name) LIKE norm(?)
|
||||
AND u.profil_sichtbarkeit != 'private'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM friendships f
|
||||
WHERE (f.requester_id=? AND f.addressee_id=u.id)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
"""BAN YARO — KI Routes"""
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
import ki as ki_module
|
||||
from auth import get_current_user
|
||||
from ratelimit import check as rl_check
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
|
@ -14,11 +16,10 @@ class TrainingRequest(BaseModel):
|
|||
|
||||
|
||||
@router.post("/training")
|
||||
async def ki_training(req: TrainingRequest):
|
||||
"""
|
||||
KI-Trainingsberatung für individuelle Verhaltens- und Trainingsprobleme.
|
||||
Kostenlos für alle (nutzt lokales Modell).
|
||||
"""
|
||||
async def ki_training(req: TrainingRequest, request: Request,
|
||||
user=Depends(get_current_user)):
|
||||
"""KI-Trainingsberatung für individuelle Verhaltens- und Trainingsprobleme."""
|
||||
rl_check(request, max_requests=10, window_seconds=3600, key="ki_training")
|
||||
if not req.problem or len(req.problem.strip()) < 10:
|
||||
raise HTTPException(400, "Bitte beschreibe das Problem genauer.")
|
||||
if len(req.problem) > 1000:
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@
|
|||
|
||||
import os, uuid, math
|
||||
from datetime import datetime, timedelta
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile, File
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from database import db
|
||||
from auth import get_current_user
|
||||
from routes.push import send_push_to_all
|
||||
from media_utils import convert_media
|
||||
from ratelimit import check as rl_check
|
||||
|
||||
router = APIRouter()
|
||||
MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
|
||||
|
|
@ -74,7 +75,9 @@ async def list_poison(lat: float, lon: float, radius: int = 5000):
|
|||
# POST /api/poison — neue Meldung (Login erforderlich)
|
||||
# ------------------------------------------------------------------
|
||||
@router.post("", status_code=201)
|
||||
async def report_poison(data: PoisonCreate, user=Depends(get_current_user)):
|
||||
async def report_poison(data: PoisonCreate, request: Request,
|
||||
user=Depends(get_current_user)):
|
||||
rl_check(request, max_requests=3, window_seconds=3600, key="poison")
|
||||
expires_at = (datetime.utcnow() + timedelta(days=7)).isoformat()
|
||||
with db() as conn:
|
||||
conn.execute(
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ import os
|
|||
import shutil
|
||||
import time
|
||||
import logging
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, UploadFile, File
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel
|
||||
from database import db
|
||||
from auth import get_current_user, get_current_user_optional
|
||||
from ratelimit import check as rl_check, block_ip
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
|
||||
|
|
@ -17,6 +19,17 @@ SUBMIT_DIR = os.path.join(BREEDS_DIR, "submissions")
|
|||
router = APIRouter()
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Honeypot — URL die kein echter Browser aufruft
|
||||
# GET /api/wiki/trap → IP 24h sperren
|
||||
# ------------------------------------------------------------------
|
||||
@router.get("/trap", include_in_schema=False)
|
||||
async def honeypot(request: Request):
|
||||
block_ip(request, hours=24)
|
||||
logger.warning("Honeypot ausgelöst von %s", request.client.host if request.client else "?")
|
||||
raise HTTPException(404, "Not found")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Schemas
|
||||
# ------------------------------------------------------------------
|
||||
|
|
@ -82,11 +95,13 @@ async def get_stats():
|
|||
# ------------------------------------------------------------------
|
||||
@router.get("/rassen")
|
||||
async def get_rassen(
|
||||
request: Request,
|
||||
search: str = Query(""),
|
||||
gruppe: str = Query(""),
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
offset: int = Query(0, ge=0),
|
||||
):
|
||||
rl_check(request, max_requests=60, window_seconds=60, key="wiki_list")
|
||||
conditions = []
|
||||
args = []
|
||||
|
||||
|
|
@ -131,7 +146,8 @@ async def get_rassen(
|
|||
# GET /api/wiki/rassen/{slug} — Rasse-Detail + Community-Berichte
|
||||
# ------------------------------------------------------------------
|
||||
@router.get("/rassen/{rasse_slug}")
|
||||
async def get_rasse(rasse_slug: str):
|
||||
async def get_rasse(rasse_slug: str, request: Request):
|
||||
rl_check(request, max_requests=30, window_seconds=60, key="wiki_detail")
|
||||
with db() as conn:
|
||||
rasse = conn.execute(
|
||||
"SELECT * FROM wiki_rassen WHERE slug = ?", (rasse_slug,)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue