@bobfrankston/rmfmail 1.2.298 → 1.2.299
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.commitmsg +53 -33
- package/client/android-bootstrap.bundle.js +23 -6
- package/client/android-bootstrap.bundle.js.map +2 -2
- package/client/app.bundle.js.map +1 -1
- package/npmchanges.md +36 -0
- package/package.json +5 -5
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-settings/package.json +1 -1
- package/packages/mailx-store/package.json +1 -1
- package/packages/mailx-store-web/package.json +1 -1
- package/packages/mailx-types/package.json +1 -1
- package/packages/mailx-types/trust.d.ts.map +1 -1
- package/packages/mailx-types/trust.js +60 -7
- package/packages/mailx-types/trust.js.map +1 -1
- package/packages/mailx-types/trust.ts +57 -5
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-39180 → node_modules.npmglobalize-stash-67432}/.package-lock.json +0 -0
package/.commitmsg
CHANGED
|
@@ -1,33 +1,53 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
1
|
+
Laundering needs a second party — a redirector alone is not one
|
|
2
|
+
|
|
3
|
+
Google's account-security notice, which is exactly the mail a reader must be
|
|
4
|
+
able to believe, carried a caution banner: "A link hides where it goes: it
|
|
5
|
+
passes through accounts.google.com and ends at myaccount.google.com." Both
|
|
6
|
+
halves of that sentence name Google. Bob, on the message:
|
|
7
|
+
`mailxstore/bobma/e1/e193f2bb….eml`, "this is valid."
|
|
8
|
+
|
|
9
|
+
The check compared `target.hostname === url.hostname` and reported anything
|
|
10
|
+
else. That catches the case it was written for — `castanet-sa.fr/redirect.php
|
|
11
|
+
?newurl=https://evil.example`, someone's abandoned redirect script spending a
|
|
12
|
+
real business's reputation — but the comparison is not what the finding
|
|
13
|
+
claims. Laundering requires a SECOND PARTY, someone whose name is on the
|
|
14
|
+
visible link without their consent. Two ways a link has none, both now checked,
|
|
15
|
+
neither of them a threshold:
|
|
16
|
+
|
|
17
|
+
1. The redirector and the destination are the same organisation. Equality
|
|
18
|
+
widened to the sub/parent-domain test this file already uses twice, now a
|
|
19
|
+
shared `sameOrg()` — deliberately not "the last two labels match", which
|
|
20
|
+
would make paypal.co.uk and evil.co.uk relatives.
|
|
21
|
+
|
|
22
|
+
2. The redirector IS the sender, proved. accounts.google.com and
|
|
23
|
+
myaccount.google.com are siblings, so rule 1 does not reach them; what
|
|
24
|
+
clears the message is that the receiving server recorded `dmarc=pass
|
|
25
|
+
header.from=accounts.google.com`. The host in the visible link belongs to
|
|
26
|
+
the party that provably sent the mail. Whether that party is trustworthy
|
|
27
|
+
is a different question than the one this check asks.
|
|
28
|
+
|
|
29
|
+
The proof comes from `provedSenderDomain()`, which asks dmarcProof first — an
|
|
30
|
+
Authentication-Results header alone is enough, so this works on Gmail-API mail
|
|
31
|
+
with no SpamAssassin headers — and falls back to the DKIM_VALID_AU /
|
|
32
|
+
ALL_TRUSTED proofs SpamAssassin states in its own rule list. It reads the
|
|
33
|
+
topmost Authentication-Results only, which the sender cannot write. No proof
|
|
34
|
+
means the check stays on: an escape a forger could open by typing a domain
|
|
35
|
+
into From would be worse than no check.
|
|
36
|
+
|
|
37
|
+
The mirror rule is NOT safe and is deliberately absent. "The link ENDS at the
|
|
38
|
+
proved sender" would clear `legit-bank.com/r?u=https://evil.com` sent from a
|
|
39
|
+
DMARC-proved evil.com, which is the laundering case in its purest form — the
|
|
40
|
+
reader sees the bank's hostname. Only the visible host may be the sender's.
|
|
41
|
+
|
|
42
|
+
Measured over the 800 most recent messages in the store: redirector-link fires
|
|
43
|
+
15 times before, 4 after. Cleared: Substack's own link wrapper on Substack
|
|
44
|
+
mail (6), Google's alert, Microsoft Teams, dev.to. Still flagged, correctly by
|
|
45
|
+
this rule's terms: Microsoft's Safe Links wrapper (nam.safelink.emails.azure.net
|
|
46
|
+
-> admin.microsoft.com, 3) and a donation platform bouncing back to the charity
|
|
47
|
+
that mailed. Those are third-party hostnames in front of the reader, and no
|
|
48
|
+
hostname allowlist is going in to quiet them.
|
|
49
|
+
|
|
50
|
+
Four tests: the sibling-subdomain case, the Google alert with its proof and
|
|
51
|
+
again with the proof removed, and a forged accounts.google.com whose server
|
|
52
|
+
recorded the DMARC failure — still flagged, as the escape is the server's
|
|
53
|
+
verdict and never the From line's say-so.
|
|
@@ -2226,6 +2226,13 @@ function bare(addr) {
|
|
|
2226
2226
|
const m = (addr || "").match(/[^\s<>,;]+@[^\s<>,;]+/);
|
|
2227
2227
|
return (m ? m[0] : addr || "").toLowerCase().replace(/[.,;]+$/, "");
|
|
2228
2228
|
}
|
|
2229
|
+
function sameOrg(a, b) {
|
|
2230
|
+
const x = (a || "").toLowerCase();
|
|
2231
|
+
const y = (b || "").toLowerCase();
|
|
2232
|
+
if (!x || !y)
|
|
2233
|
+
return false;
|
|
2234
|
+
return x === y || x.endsWith("." + y) || y.endsWith("." + x);
|
|
2235
|
+
}
|
|
2229
2236
|
function classifyRule(name, dmarcFailed) {
|
|
2230
2237
|
if (dmarcFailed && name === "DKIM_INVALID")
|
|
2231
2238
|
return "forgery";
|
|
@@ -2455,7 +2462,17 @@ function hiddenLinkOverlay(bodyHtml) {
|
|
|
2455
2462
|
}
|
|
2456
2463
|
return null;
|
|
2457
2464
|
}
|
|
2458
|
-
function
|
|
2465
|
+
function provedSenderDomain(input) {
|
|
2466
|
+
const fromDomain = (bare(input.fromAddress).split("@")[1] || "").toLowerCase();
|
|
2467
|
+
if (!fromDomain)
|
|
2468
|
+
return "";
|
|
2469
|
+
if (dmarcProof(input).pass)
|
|
2470
|
+
return fromDomain;
|
|
2471
|
+
return analyzeServerSpam(input)?.proved ? fromDomain : "";
|
|
2472
|
+
}
|
|
2473
|
+
function redirectorLink(input) {
|
|
2474
|
+
const bodyHtml = input.bodyHtml || "";
|
|
2475
|
+
const senderDomain = provedSenderDomain(input);
|
|
2459
2476
|
for (const m of bodyHtml.matchAll(/href\s*=\s*["']([^"']+)["']/gi)) {
|
|
2460
2477
|
let url;
|
|
2461
2478
|
try {
|
|
@@ -2465,6 +2482,8 @@ function redirectorLink(bodyHtml) {
|
|
|
2465
2482
|
}
|
|
2466
2483
|
if (!/^https?:$/.test(url.protocol))
|
|
2467
2484
|
continue;
|
|
2485
|
+
if (senderDomain && sameOrg(url.hostname, senderDomain))
|
|
2486
|
+
continue;
|
|
2468
2487
|
for (const [, value] of url.searchParams) {
|
|
2469
2488
|
let target;
|
|
2470
2489
|
try {
|
|
@@ -2474,7 +2493,7 @@ function redirectorLink(bodyHtml) {
|
|
|
2474
2493
|
}
|
|
2475
2494
|
if (!/^https?:$/.test(target.protocol))
|
|
2476
2495
|
continue;
|
|
2477
|
-
if (target.hostname
|
|
2496
|
+
if (sameOrg(target.hostname, url.hostname))
|
|
2478
2497
|
continue;
|
|
2479
2498
|
return {
|
|
2480
2499
|
id: "redirector-link",
|
|
@@ -2493,7 +2512,7 @@ function assessMessageTrust(input) {
|
|
|
2493
2512
|
relayAuthMismatch(input),
|
|
2494
2513
|
zeroWidthObfuscation(input.bodyText || ""),
|
|
2495
2514
|
hiddenLinkOverlay(input.bodyHtml || ""),
|
|
2496
|
-
redirectorLink(input
|
|
2515
|
+
redirectorLink(input)
|
|
2497
2516
|
].filter(Boolean);
|
|
2498
2517
|
const rank = { danger: 0, caution: 1, info: 2 };
|
|
2499
2518
|
return findings.sort((a, b) => rank[a.severity] - rank[b.severity]);
|
|
@@ -2506,9 +2525,7 @@ function relayAuthMismatch(input) {
|
|
|
2506
2525
|
for (const name of RELAY_IDENTITY_HEADERS) {
|
|
2507
2526
|
const stamped = bare(header(input.headerLines, name));
|
|
2508
2527
|
const stampedDomain = stamped.split("@")[1] || "";
|
|
2509
|
-
if (!stampedDomain || stampedDomain
|
|
2510
|
-
continue;
|
|
2511
|
-
if (stampedDomain.endsWith("." + fromDomain) || fromDomain.endsWith("." + stampedDomain))
|
|
2528
|
+
if (!stampedDomain || sameOrg(stampedDomain, fromDomain))
|
|
2512
2529
|
continue;
|
|
2513
2530
|
return {
|
|
2514
2531
|
id: "relay-auth-mismatch",
|