aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-02 17:41:39 +0200
committerDanilo M. <danix@danix.xyz>2026-08-02 17:41:39 +0200
commit8ecd5a8e83380f543b856ff28226a8e15fd7dc96 (patch)
tree1201e7311e36ba80db2aafe0d182d9b260f3ad61
parent485e06b796603ef760576748fbd96bc177d5fa49 (diff)
downloadqtmaildir-8ecd5a8e83380f543b856ff28226a8e15fd7dc96.tar.gz
qtmaildir-8ecd5a8e83380f543b856ff28226a8e15fd7dc96.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>
-rw-r--r--docs/superpowers/plans/2026-08-02-qtmaildir-v1.md25
-rw-r--r--docs/superpowers/specs/2026-08-02-qtmaildir-design.md9
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<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();
}
diff --git a/docs/superpowers/specs/2026-08-02-qtmaildir-design.md b/docs/superpowers/specs/2026-08-02-qtmaildir-design.md
index a2b4955..e153203 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