diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-02 17:41:39 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-04 12:52:28 +0200 |
| commit | 5690c50ff9a6ef4555c2c4ccd9a7dbd7dc172c7a (patch) | |
| tree | 90da159f0ae913ee8f6d1a12ddc0d70faa31183b /docs/superpowers/plans/2026-08-02-qtmaildir-v1.md | |
| parent | 14843569125f7aeb4adaf547c323c8eb18961461 (diff) | |
| download | qtmaildir-5690c50ff9a6ef4555c2c4ccd9a7dbd7dc172c7a.tar.gz qtmaildir-5690c50ff9a6ef4555c2c4ccd9a7dbd7dc172c7a.zip | |
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 <noreply@anthropic.com>
Diffstat (limited to 'docs/superpowers/plans/2026-08-02-qtmaildir-v1.md')
| -rw-r--r-- | docs/superpowers/plans/2026-08-02-qtmaildir-v1.md | 25 |
1 files changed, 22 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<QString> &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<QString> 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<ThreadRenderItem> &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(); } |
