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:
1 parent
419d200bf9
commit
bb20135876
11 files changed
+367
-61
No files matched your search
+4
-29
@@ -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}
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user