From 5690c50ff9a6ef4555c2c4ccd9a7dbd7dc172c7a Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Sun, 2 Aug 2026 17:41:39 +0200 Subject: docs: scope the document-load exemption to an exact URL The interceptor previously trusted the whole qtmaildir: scheme, so a hostile message body referencing qtmaildir://anything would have been allowed. That made the interceptor's correctness depend on the scheme handler in a different, later task. It now trusts only the exact base URL passed to setHtml() and fails closed when none is set. Records the resulting obligation on MessageView to call setDocumentUrl() with that same URL. Also corrects the attachment path guard to a separator-aware comparison and notes that it is unreachable defence-in-depth: safeFilename() runs first, so no caller-supplied name can reach it. A test driving saveTo() expecting refusal cannot pass; test safeFilename() instead. Co-Authored-By: Claude Opus 5 --- docs/superpowers/plans/2026-08-02-qtmaildir-v1.md | 25 +++++++++++++++++++--- .../specs/2026-08-02-qtmaildir-design.md | 9 ++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/docs/superpowers/plans/2026-08-02-qtmaildir-v1.md b/docs/superpowers/plans/2026-08-02-qtmaildir-v1.md index 391bcb6..b2c3307 100644 --- a/docs/superpowers/plans/2026-08-02-qtmaildir-v1.md +++ b/docs/superpowers/plans/2026-08-02-qtmaildir-v1.md @@ -1636,6 +1636,11 @@ public: /// Content-IDs belonging to the currently displayed message. void setAllowedCids(const QSet &cids) { m_allowedCids = cids; } + /// The exact base URL passed to setHtml(). REQUIRED: without it every + /// qtmaildir: URL is denied and nothing renders. Only this exact URL is + /// trusted on that scheme; the scheme alone is not sufficient. + void setDocumentUrl(const QUrl &url) { m_documentUrl = url; } + /// Per-message opt-in, triggered by the user clicking "Load remote content". /// Never persisted, never carried to the next message. void setAllowRemote(bool allow) { m_allowRemote = allow; } @@ -1650,6 +1655,7 @@ public: private: QSet m_allowedCids; + QUrl m_documentUrl; bool m_allowRemote = false; bool m_blockedAnything = false; }; @@ -1672,9 +1678,17 @@ bool RequestInterceptor::shouldAllow(const QUrl &url) const QString scheme = url.scheme(); // The document itself is loaded via setHtml() with a qtmaildir: base URL, - // so that scheme must pass or nothing renders at all. - if (scheme == QLatin1String("qtmaildir")) - return true; + // so that exact URL must pass or nothing renders at all. Allowing the + // whole SCHEME would be a hole: a hostile body could reference + // qtmaildir://anything and be trusted, which would make this object's + // correctness depend on the scheme handler's. Fails closed when no + // document URL has been set. + if (scheme == QLatin1String("qtmaildir")) { + if (!m_documentUrl.isEmpty() && url == m_documentUrl) + return true; + m_blockedAnything = true; + return false; + } // Inline parts of the current message only. if (scheme == QLatin1String("cid")) { @@ -3510,6 +3524,11 @@ void MessageView::showThread(const QList &items) m_interceptor->setAllowedCids(cids); m_cidHandler->setParts(allParts); + // REQUIRED by RequestInterceptor: it trusts only this exact URL on the + // qtmaildir: scheme and fails closed otherwise, so this must match the + // base URL passed to setHtml() in render() or nothing renders at all. + m_interceptor->setDocumentUrl(QUrl(QStringLiteral("qtmaildir://message"))); + updateHeader(); render(); } diff --git a/docs/superpowers/specs/2026-08-02-qtmaildir-design.md b/docs/superpowers/specs/2026-08-02-qtmaildir-design.md index 1edc75f..0f49b61 100644 --- a/docs/superpowers/specs/2026-08-02-qtmaildir-design.md +++ b/docs/superpowers/specs/2026-08-02-qtmaildir-design.md @@ -312,6 +312,15 @@ pointed at input from strangers. from the current message's own parts are permitted. Remote images, CSS, and fonts are blocked before a connection opens, which defeats tracking pixels and read receipts. + + The document-load exemption is scoped to the **exact** base URL passed to + `setHtml()`, not to the whole `qtmaildir:` scheme. A blanket + scheme-level allow would let a hostile body reference + `qtmaildir://anything` and have it trusted, making the interceptor's + correctness depend on the scheme handler's. The interceptor fails closed: + with no document URL set, every `qtmaildir:` URL is denied. `MessageView` + must therefore call `setDocumentUrl()` with the same URL it gives + `setHtml()`. - When anything was blocked, the header bar shows "Remote content blocked" with a **Load remote content** button. Clicking it re-renders that one message with remote loads permitted. The grant is never sticky and never -- cgit v1.2.3