Datenschutz: Bild-Metadaten, private Vorschläge, fremde Bilder, API-Doku

- Uploads: Server entfernt EXIF/XMP/Text (GPS-Standort, Kamera, Uhrzeit)
  per Pillow; Drehung von Hochkant-Fotos wird vorher angewendet, Farbprofil
  bleibt; GIFs unverändert (neue Abhängigkeit pillow)
- Produktvorschläge pro Liste (Tabelle list_products): Einträge privater
  Listen tauchen bei anderen weder in Vorschlägen noch beim Rezeptabgleich
  auf; "Vorschläge verwalten" löscht nur aus eigenen sichtbaren Listen
- Artikel: Bilder nur noch aus den eigenen Uploads (kein Tracking-Pixel
  fremder Server); Bereinigung in htmlclean.py
- /api/docs und /api/openapi.json nur noch im Entwicklungsmodus
  (RELOAD=1 oder P42_API_DOCS=1)
- Migration Schritt 2: bestehende Vorschläge den Listen zuordnen, Artikel
  neu bereinigen, gespeicherte Bilder mit Metadaten säubern
- Tests in test_datenschutz.py (u. a. Rezept-Zuordnung, Umstellung)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
guyg1474andClaude Opus 5.5 committed 2026-10-15 18:48:19 +02:00
1 parent 419d200bf9
commit bb20135876
11 files changed
+367 -61

No files matched your search

+4 -29
View File
@@ -3,14 +3,15 @@ import re
import secrets
import time
import nh3
from fastapi import APIRouter, Body, Depends, HTTPException, Request
from fastapi.responses import FileResponse
from .auth import current_user
from .bilder import ohne_metadaten
from .config import DATA_DIR
from .common import ValidationError, clean_text, now_iso
from .db import transaction
from .htmlclean import sanitize
from .kanban import boards_for_article
router = APIRouter()
@@ -26,33 +27,6 @@ MAX_UPLOAD = 15 * 1024 * 1024
MIME = {"jpg": "image/jpeg", "png": "image/png", "gif": "image/gif", "webp": "image/webp"}
# ---------- Artikel-HTML bereinigen ----------
ALLOWED_TAGS = {"p", "br", "h2", "h3", "strong", "b", "em", "i", "u", "s", "ul", "ol", "li", "blockquote", "hr", "a", "img"}
ALLOWED_ATTRS = {"a": {"href"}, "img": {"src", "alt"}}
def _attribute_filter(tag: str, attr: str, value: str) -> str | None:
# Bilder nur aus den eigenen Uploads oder per https
if tag == "img" and attr == "src" and not (value.startswith("/uploads/") or value.startswith("https://")):
return None
return value
def sanitize(content: str) -> str:
cleaned = nh3.clean(
content,
tags=ALLOWED_TAGS,
clean_content_tags={"script", "style"},
attributes=ALLOWED_ATTRS,
attribute_filter=_attribute_filter,
url_schemes={"http", "https", "mailto"},
link_rel="noopener noreferrer",
)
# Bilder, deren Quelle entfernt wurde, ganz weglassen
return re.sub(r"<img(?![^>]*\ssrc=)[^>]*>", "", cleaned).strip()
# ---------- Validierung ----------
def _text(value, field: str, max_len: int, required: bool = False) -> str:
@@ -247,7 +221,8 @@ async def upload(request: Request, _: dict = Depends(current_user)):
if not ext:
raise ValidationError("Bildformat nicht unterstützt (erlaubt: JPG, PNG, GIF, WebP).")
name = f"{secrets.token_hex(12)}.{ext}"
(UPLOAD_DIR / name).write_bytes(data)
# GPS-Standort, Gerät usw. entfernen: die Bilder sehen alle Konten
(UPLOAD_DIR / name).write_bytes(ohne_metadaten(bytes(data), ext))
return {"file": name}