diff --git a/content/browser/renderer_host/clipboard_host_impl.cc b/content/browser/renderer_host/clipboard_host_impl.cc index 031adcc55f5e9..4a2d6c827b824 100644 --- a/content/browser/renderer_host/clipboard_host_impl.cc +++ b/content/browser/renderer_host/clipboard_host_impl.cc @@ -666,6 +666,18 @@ bool ClipboardHostImpl::IsRendererPasteAllowed( void ClipboardHostImpl::ReadAvailableCustomAndStandardFormats( ReadAvailableCustomAndStandardFormatsCallback callback) { + // --------------------------------------------------------------------------- + // POC SCAFFOLDING (TOCTOU demonstration): + // Seed the OS clipboard with known "ORIGINAL" content immediately before the + // format-name enumeration. This is the clipboard state the user authorised + // when navigator.clipboard.read() was called (sequence N). + // --------------------------------------------------------------------------- + { + ui::ScopedClipboardWriter w(ui::ClipboardBuffer::kCopyPaste); + w.WriteText(u"ORIGINAL-DATA-SEQ-N"); + } + LOG(ERROR) << "[POC] Format enumeration sees clipboard@N (ORIGINAL)."; + if (!IsRendererPasteAllowed(ui::ClipboardBuffer::kCopyPaste, render_frame_host())) { std::move(callback).Run(std::vector()); @@ -680,6 +692,23 @@ void ClipboardHostImpl::ReadAvailableCustomAndStandardFormats( [](std::optional data_endpoint, ReadAvailableCustomAndStandardFormatsCallback callback, std::vector format_types) { + // ------------------------------------------------------------- + // POC SCAFFOLDING (TOCTOU demonstration): + // Format names have just been read from clipboard@N. Now + // simulate the user copying SECRET data in another application + // BEFORE the renderer's ResolveRead() captures the sequence + // number via a separate GetSequenceNumber IPC. This is the + // exact race window described in the report. In an unpatched + // build, GetSequenceNumber will observe sequence M, and the + // resulting ClipboardItem will be poisoned: mime_types from + // clipboard@N, sequence_number_ = M. + // ------------------------------------------------------------- + { + ui::ScopedClipboardWriter w(ui::ClipboardBuffer::kCopyPaste); + w.WriteText(u"SECRET-COPIED-AFTER-READ-SEQ-M"); + } + LOG(ERROR) << "[POC] Clipboard mutated N->M (SECRET) inside " + "TOCTOU window."; std::move(callback).Run(std::move(format_types)); }, data_endpoint, std::move(callback))); diff --git a/third_party/blink/renderer/modules/clipboard/clipboard_promise.cc b/third_party/blink/renderer/modules/clipboard/clipboard_promise.cc index a4a7dbd1c9bcc..3e5d9d2a17945 100644 --- a/third_party/blink/renderer/modules/clipboard/clipboard_promise.cc +++ b/third_party/blink/renderer/modules/clipboard/clipboard_promise.cc @@ -696,7 +696,12 @@ void ClipboardPromise::ValidatePreconditions( LocalDOMWindow& window = *To(context); DCHECK(window.IsSecureContext()); // [SecureContext] in IDL - if (!window.document()->hasFocus()) { + // POC SCAFFOLDING: bypass focus check for headless/automated reproduction. + // The real attack precondition is "page has focus when read() is called", + // which a real attacker page trivially satisfies. This is NOT part of the + // vulnerability — it just removes the need for an interactive X session. + static const bool kPocBypassFocus = true; + if (!kPocBypassFocus && !window.document()->hasFocus()) { script_promise_resolver_->RejectWithDOMException( DOMExceptionCode::kNotAllowedError, "Document is not focused."); return;