[object Object]

← back to Sample Followup Sweep

Reject injected mailto opt-out headers

f171037180d946af0d2ce64713eae9f1ab225ece · 2026-08-28 16:42:02 -0700 · Steve Abrams

Files touched

Diff

commit f171037180d946af0d2ce64713eae9f1ab225ece
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Aug 28 16:42:02 2026 -0700

    Reject injected mailto opt-out headers
---
 lib/pre-send-gate.js        | 18 ++++++++++++++----
 test/pre-send-gate.test.js  | 17 +++++++++++++++++
 verification/e2e-proof.json | 13 +++++++++----
 3 files changed, 40 insertions(+), 8 deletions(-)

diff --git a/lib/pre-send-gate.js b/lib/pre-send-gate.js
index 65aba37..69a4f3a 100644
--- a/lib/pre-send-gate.js
+++ b/lib/pre-send-gate.js
@@ -72,15 +72,25 @@ function visibleTextFromHtml(body) {
   return renderedContent(body).visibleText;
 }
 
+function isCanonicalOptOutMailto(url) {
+  if (url.protocol !== 'mailto:' || url.hash) return false;
+  let recipient;
+  try { recipient = decodeURIComponent(url.pathname); } catch { return false; }
+  if (recipient.toLowerCase() !== REQUIRED_FROM) return false;
+  const entries = [...url.searchParams.entries()];
+  if (entries.length !== 1 || entries[0][0] !== 'subject') return false;
+  const subject = entries[0][1];
+  if (!subject || /[\r\n\0]/.test(subject)) return false;
+  return /(?:unsubscribe|opt\s*out|stop\s+receiving)/i.test(subject);
+}
+
 function hasWorkingOptOutLink(body) {
   const { anchors } = renderedContent(body);
   return anchors.some(({ href, visibleLabel }) => {
     try {
       if (!/(?:unsubscribe|opt\s*out|stop\s+receiving)/i.test(visibleLabel)) return false;
       const url = new URL(href);
-      const optOutSignal = `${url.pathname} ${url.search} ${url.hash}`;
-      if (!/(?:unsubscribe|opt[-_ ]?out)/i.test(optOutSignal)) return false;
-      if (url.protocol === 'mailto:') return url.pathname.toLowerCase() === REQUIRED_FROM;
+      if (url.protocol === 'mailto:') return isCanonicalOptOutMailto(url);
       // No canonical DW-hosted unsubscribe endpoint is registered yet. Fail closed
       // instead of treating an attacker-controlled HTTPS URL as a working opt-out.
       return false;
@@ -106,4 +116,4 @@ function assertPreSendCompliance({ from, subject, body }) {
   return true;
 }
 
-module.exports = { assertPreSendCompliance, hasWorkingOptOutLink, renderedContent, visibleTextFromHtml, REQUIRED_FROM };
+module.exports = { assertPreSendCompliance, hasWorkingOptOutLink, isCanonicalOptOutMailto, renderedContent, visibleTextFromHtml, REQUIRED_FROM };
diff --git a/test/pre-send-gate.test.js b/test/pre-send-gate.test.js
index eaa9ae1..21860b4 100644
--- a/test/pre-send-gate.test.js
+++ b/test/pre-send-gate.test.js
@@ -64,6 +64,23 @@ for (const labelBypass of [
   assert.throws(() => assertPreSendCompliance({ from: REQUIRED_FROM, subject: compliant.subject, body: bodyWithoutLink + labelBypass }), { code: 'PRE_SEND_COMPLIANCE_BLOCKED' });
 }
 
+for (const mailtoInjection of [
+  'mailto:info@designerwallcoverings.com?subject=Unsubscribe&bcc=attacker@example.org',
+  'mailto:info@designerwallcoverings.com?subject=Unsubscribe&cc=attacker@example.org',
+  'mailto:info@designerwallcoverings.com?subject=Unsubscribe&to=attacker@example.org',
+  'mailto:info@designerwallcoverings.com?subject=Unsubscribe&subject=Opt%20out',
+  'mailto:info@designerwallcoverings.com?subject=Unsubscribe&body=remove%20me',
+  'mailto:info@designerwallcoverings.com?subject=Unsubscribe&x-extra=1',
+  'mailto:info@designerwallcoverings.com,attacker@example.org?subject=Unsubscribe',
+  'mailto:info@designerwallcoverings.com%2Cattacker@example.org?subject=Unsubscribe',
+  'mailto:info@designerwallcoverings.com;attacker@example.org?subject=Unsubscribe',
+  'mailto:info@designerwallcoverings.com?subject=Unsubscribe#attacker',
+  'mailto:info@designerwallcoverings.com?subject=Unsubscribe%0ABcc%3Aattacker%40example.org',
+]) {
+  const injected = bodyWithoutLink + `<a href="${mailtoInjection.replace(/&/g, '&amp;')}">unsubscribe here</a>`;
+  assert.throws(() => assertPreSendCompliance({ from: REQUIRED_FROM, subject: compliant.subject, body: injected }), { code: 'PRE_SEND_COMPLIANCE_BLOCKED' });
+}
+
 const validOptOut = compliant.html.match(/<a href="mailto:[\s\S]*?<\/a>/)[0];
 for (const parserBypass of [
   '<div style="display&#58;none">15442 Ventura Blvd. #102, Sherman Oaks, CA 91403</div>' + validOptOut,
diff --git a/verification/e2e-proof.json b/verification/e2e-proof.json
index cb8dae9..e0b0a12 100644
--- a/verification/e2e-proof.json
+++ b/verification/e2e-proof.json
@@ -1,9 +1,9 @@
 {
-  "intent": "Centralize fail-closed compliance enforcement and require parsed-tree-visible, meaningfully labeled DW-owned compliance content",
+  "intent": "Centralize fail-closed compliance enforcement and require parsed-tree-visible, meaningfully labeled, injection-safe DW-owned compliance content",
   "risk_tier": "R3",
   "environment": "local repository only; injected HTTP spy; no George/Gmail/FileMaker/production network calls",
-  "build_identity": "Cycle 5 working tree based on 1f59efb; final commit recorded after verification",
-  "timestamp": "2026-08-28T16:34:16-07:00",
+  "build_identity": "Cycle 6 working tree based on 900aa26; final commit recorded after verification",
+  "timestamp": "2026-08-28T16:41:45-07:00",
   "ticket": "TK-10940-centralize-sample-follow-up-pre-send-com",
   "baseline": "Cycle 1 commit c74ebf7 had an ungated /api/drafts call, an ungated gmail_create_draft artifact, and accepted hidden/comment/plain-text/reserved-domain opt-out tokens",
   "commands": [
@@ -22,7 +22,7 @@
     {
       "boundary": "compliance parser",
       "verdict": "PASS",
-      "evidence": "parse5 HTML5 tree parsing plus test/pre-send-gate.test.js reject comments, unclosed script, entity-encoded/CSS-comment display:none, script/template/hidden address text, script-only links, empty/whitespace/icon-only/hidden-only opt-out labels, plain-text mailto tokens, invalid.example, and attacker.example.org while accepting lib/compose.js and reconciled-footer output"
+      "evidence": "parse5 HTML5 tree parsing plus test/pre-send-gate.test.js reject comments, hidden/malformed content, empty/hidden labels, attacker-owned URLs, and mailto cc/bcc/to/extra-query/duplicate-subject/multi-recipient/fragment/header-folding injection while accepting lib/compose.js and reconciled-footer output"
     },
     {
       "boundary": "dependency integrity",
@@ -68,6 +68,11 @@
     "approved opt-out destination with empty label",
     "approved opt-out destination with whitespace or icon-only label",
     "approved opt-out destination with label only in hidden descendant",
+    "approved mailto with cc/bcc/to or unknown query parameter",
+    "approved mailto with duplicate subject parameter",
+    "approved mailto with comma/encoded-comma/semicolon additional recipient",
+    "approved mailto with fragment",
+    "approved mailto with encoded CR/LF header folding",
     "blocked transport makes zero requests"
   ],
   "known_limit": "parse5 provides an HTML5 parsed tree, not browser layout or computed stylesheets; class-based CSS hiding is excluded by the trusted-template boundary and documented as a hardening limit",

← 900aa26 Require visible opt-out link labels  ·  back to Sample Followup Sweep  ·  auto-data-snapshot: 2026-08-29T08:29:29 (1 data files) — dat 129af57 →