From c344de8bfad1fd07ed179e230e053e1cd7ae05dc Mon Sep 17 00:00:00 2001 From: duffyduck Date: Thu, 30 Jul 2026 12:39:45 +0200 Subject: [PATCH] PdfDragButton: Drag nie abbrechen (Regression nach R131-Fix) Der R131-Fix hatte den Drag per preventDefault() abgebrochen, wenn der 60s-Download-Token beim Griff noch nicht vorgewaermt war -> im Mail- Fenster kam gar keine Datei mehr an. Jetzt wird der Drag nie abgebrochen: bevorzugt der vorgewaermte Download-Token, im seltenen Rennen Fallback auf den Access-Token, aber weiterhin NUR in DownloadURL (kein text/plain) -> kein Klartext-Leak. DownloadURL ist ein Nativ-Format und wird bei Fehl-Drop in Web-Text- felder nicht als lesbarer Text ausgegeben, daher bleibt der R131-Fix (kein Token-Leak als Text) erhalten. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/components/ui/PdfDragButton.tsx | 45 +++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/frontend/src/components/ui/PdfDragButton.tsx b/frontend/src/components/ui/PdfDragButton.tsx index a115d0ac..dfd41e0b 100644 --- a/frontend/src/components/ui/PdfDragButton.tsx +++ b/frontend/src/components/ui/PdfDragButton.tsx @@ -1,7 +1,7 @@ import { useEffect } from 'react'; import { GripVertical, FileDown, Info } from 'lucide-react'; import { fileUrl } from '../../utils/fileUrl'; -import { authApi } from '../../services/api'; +import { authApi, getAccessToken } from '../../services/api'; /** * Ziehbares Element, mit dem eine im Backend hinterlegte Datei (i.d.R. ein @@ -23,14 +23,18 @@ import { authApi } from '../../services/api'; * Fallback (Datei im neuen Tab öffnen) bleibt aber nutzbar. * * SICHERHEIT (Pentest R131): - * - Für die Drag-URL wird ein KURZLEBIGER 60s-Download-Token + * - Es wird AUSSCHLIESSLICH `DownloadURL` gesetzt, KEINE `text/plain`- + * oder `text/uri-list`-Repräsentation. `DownloadURL` ist ein Chromium- + * internes Nativ-Format, das beim Fehl-Drop in ein Web-Textfeld nicht + * als lesbarer Text ankommt. Damit kann – anders als in der ersten + * Version – kein Token als Klartext in Chat/Ticket/URL-Leiste leaken. + * - In die URL wird bevorzugt ein KURZLEBIGER 60s-Download-Token * (`authApi.getDownloadToken()`, `type:"download"`, nur `?token=` - * nutzbar) verwendet – NICHT der 15-Minuten-Access-Token mit vollen - * Berechtigungen. Landet ein Drop versehentlich in einem Textfeld, - * kann bestenfalls ein 60s gültiger Downloads-only-Token auftauchen. - * - Es wird AUSSCHLIESSLICH `DownloadURL` gesetzt, keine `text/plain`- - * oder `text/uri-list`-Repräsentation. Ein Fehl-Drop in ein Text-/ - * Chat-/URL-Feld erzeugt daher gar keinen sichtbaren Text. + * nutzbar) geschrieben statt des 15-Min-Access-Tokens. Er wird + * proaktiv (mount + hover) vorgewärmt. Nur im seltenen Rennen „Drag + * bevor der Token da ist" fällt es auf den Access-Token zurück – + * und auch dann nur innerhalb von DownloadURL, nie als Klartext. + * Der Drag wird NIE abgebrochen (sonst käme im Ziel keine Datei an). */ interface PdfDragButtonProps { /** Server-Pfad der Datei (`documentPath`). Ohne Pfad wird nichts gerendert. */ @@ -110,21 +114,20 @@ export default function PdfDragButton({ const name = safeBase.includes('.') ? safeBase : `${safeBase}${ext ? `.${ext}` : ''}`; const handleDragStart = (e: React.DragEvent) => { - // Nur der kurzlebige Download-Token wird in die Drag-URL geschrieben. - // Ist noch keiner da (Netzwerk langsamer als der erste Griff), Drag - // abbrechen und Token vorwärmen – lieber kein Drag als ein Access-Token- - // Leak. Der nächste Versuch (nach Hover) hat den Token dann. - const token = cachedToken; - if (!token) { - warmDownloadToken(); - e.preventDefault(); - return; - } + // Bevorzugt den kurzlebigen 60s-Download-Token (auf mount+hover + // vorgewärmt). Ist er im seltenen Rennen noch nicht da, fällt es auf den + // Access-Token zurück – NUR innerhalb von DownloadURL. Wichtig: der Drag + // wird NIE abgebrochen (sonst käme im Mail-Fenster gar keine Datei an). + const token = cachedToken || getAccessToken() || ''; + if (!cachedToken) warmDownloadToken(); // für den nächsten Drag vorwärmen const absolute = `${window.location.origin}${fileUrl(path, { token })}`; try { - // NUR DownloadURL – bewusst keine text/plain- oder text/uri-list- - // Repräsentation (sonst würde ein Fehl-Drop die volle URL als Text - // ausgeben). Siehe Pentest R131. + // NUR DownloadURL setzen – bewusst KEINE text/plain- oder + // text/uri-list-Repräsentation. DownloadURL ist ein Chromium-internes + // Nativ-Format und wird bei einem Fehl-Drop in ein Web-Textfeld nicht + // als lesbarer Text ausgegeben; so kann kein Token als Klartext leaken + // (Pentest R131). Für Explorer/Outlook ist DownloadURL das relevante + // Format – die entfernten Text-Formate waren dafür nie nötig. e.dataTransfer.setData('DownloadURL', `${mime}:${name}:${absolute}`); } catch { /* manche Browser werfen bei DownloadURL – dann passiert einfach nichts */