Merge pull request #202 from Coffey-Labs/feat/sender-link-warnings
Warn about outside senders, large sends and links that mislead
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
crossesRecipientThreshold,
|
||||
domainCovered,
|
||||
externalRecipients,
|
||||
internalDomains,
|
||||
isExternalSender,
|
||||
linkVerdict,
|
||||
shownDomain,
|
||||
} from "@/lib/warnings";
|
||||
|
||||
const addr = (email: string, name: string | null = null) => ({ name, email });
|
||||
|
||||
describe("internalDomains", () => {
|
||||
it("always counts your own identities, without them being configured", () => {
|
||||
// An account signed in as [email protected] warning that example.com is
|
||||
// external would be absurd, and is what an empty list would do.
|
||||
const d = internalDomains(["[email protected]", "[email protected]"], []);
|
||||
expect([...d].sort()).toEqual(["example.com", "example.org"]);
|
||||
});
|
||||
|
||||
it("adds configured domains, tolerating a leading @ and stray case", () => {
|
||||
const d = internalDomains([], ["@Partner.com", " sister.org "]);
|
||||
expect([...d].sort()).toEqual(["partner.com", "sister.org"]);
|
||||
});
|
||||
|
||||
it("ignores empty entries rather than adding an empty domain", () => {
|
||||
expect(internalDomains(["notanemail"], ["", " ", "@"]).size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("domainCovered", () => {
|
||||
const internal = internalDomains([], ["example.com"]);
|
||||
|
||||
it("covers the domain itself and its subdomains", () => {
|
||||
expect(domainCovered("example.com", internal)).toBe(true);
|
||||
expect(domainCovered("mail.example.com", internal)).toBe(true);
|
||||
expect(domainCovered("a.b.example.com", internal)).toBe(true);
|
||||
});
|
||||
|
||||
it("does not cover a domain that merely ends with the same letters", () => {
|
||||
// The whole point of matching on a dot boundary: this is the shape an
|
||||
// attacker registers.
|
||||
expect(domainCovered("notexample.com", internal)).toBe(false);
|
||||
expect(domainCovered("example.com.evil.net", internal)).toBe(false);
|
||||
});
|
||||
|
||||
it("is case-insensitive and says no to nothing", () => {
|
||||
expect(domainCovered("MAIL.EXAMPLE.COM", internal)).toBe(true);
|
||||
expect(domainCovered("", internal)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("externalRecipients", () => {
|
||||
const internal = internalDomains(["[email protected]"], []);
|
||||
|
||||
it("returns only those outside, in the order addressed", () => {
|
||||
const out = externalRecipients(
|
||||
[addr("[email protected]"), addr("[email protected]"), addr("[email protected]"), addr("[email protected]")],
|
||||
internal,
|
||||
);
|
||||
expect(out.map((a) => a.email)).toEqual(["[email protected]", "[email protected]"]);
|
||||
});
|
||||
|
||||
it("is empty when everyone is inside", () => {
|
||||
expect(externalRecipients([addr("[email protected]")], internal)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isExternalSender", () => {
|
||||
const internal = internalDomains(["[email protected]"], []);
|
||||
|
||||
it("reads the first From address", () => {
|
||||
expect(isExternalSender([addr("[email protected]")], internal)).toBe(true);
|
||||
expect(isExternalSender([addr("[email protected]")], internal)).toBe(false);
|
||||
});
|
||||
|
||||
it("claims nothing about a message with no sender", () => {
|
||||
expect(isExternalSender(null, internal)).toBe(false);
|
||||
expect(isExternalSender([], internal)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("crossesRecipientThreshold", () => {
|
||||
it("is off at zero, whatever the count", () => {
|
||||
expect(crossesRecipientThreshold(500, 0)).toBe(false);
|
||||
});
|
||||
|
||||
it("fires at the threshold and above, not below", () => {
|
||||
expect(crossesRecipientThreshold(9, 10)).toBe(false);
|
||||
expect(crossesRecipientThreshold(10, 10)).toBe(true);
|
||||
expect(crossesRecipientThreshold(11, 10)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shownDomain", () => {
|
||||
it("reads a domain out of link text that is a URL or a bare host", () => {
|
||||
expect(shownDomain("https://example.com/x")).toBe("example.com");
|
||||
expect(shownDomain("example.com")).toBe("example.com");
|
||||
expect(shownDomain(" WWW.Example.COM ")).toBe("www.example.com");
|
||||
});
|
||||
|
||||
it("reads nothing out of text that is prose", () => {
|
||||
// "click" and "here" are not claims about a destination.
|
||||
expect(shownDomain("click here")).toBeNull();
|
||||
expect(shownDomain("here")).toBeNull();
|
||||
expect(shownDomain("")).toBeNull();
|
||||
expect(shownDomain(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("linkVerdict", () => {
|
||||
const trusted = ["example.com"];
|
||||
|
||||
it("says nothing about a trusted destination", () => {
|
||||
expect(linkVerdict("https://example.com/a", "example.com", trusted)).toEqual({ warn: false });
|
||||
expect(linkVerdict("https://mail.example.com/a", null, trusted)).toEqual({ warn: false });
|
||||
});
|
||||
|
||||
it("warns about an untrusted destination", () => {
|
||||
expect(linkVerdict("https://unknown.net/a", null, trusted)).toEqual({
|
||||
warn: true,
|
||||
reason: "untrusted",
|
||||
domain: "unknown.net",
|
||||
});
|
||||
});
|
||||
|
||||
it("warns about a mismatch even when the destination is trusted", () => {
|
||||
// Trusted is not the same as being the place the text claimed.
|
||||
expect(linkVerdict("https://example.com/login", "yourbank.com", trusted)).toEqual({
|
||||
warn: true,
|
||||
reason: "mismatch",
|
||||
domain: "example.com",
|
||||
shownDomain: "yourbank.com",
|
||||
});
|
||||
});
|
||||
|
||||
it("treats a subdomain of the claimed domain as no mismatch", () => {
|
||||
expect(linkVerdict("https://login.yourbank.com/", "yourbank.com", ["yourbank.com"])).toEqual({ warn: false });
|
||||
});
|
||||
|
||||
it("leaves alone anything that is not http or https", () => {
|
||||
// mailto opens the composer; an anchor goes nowhere. Warning about these
|
||||
// is noise, and noise is how a warning stops being read.
|
||||
expect(linkVerdict("mailto:[email protected]", null, [])).toEqual({ warn: false });
|
||||
expect(linkVerdict("#section", null, [])).toEqual({ warn: false });
|
||||
expect(linkVerdict("javascript:alert(1)", null, [])).toEqual({ warn: false });
|
||||
expect(linkVerdict("not a url at all", null, [])).toEqual({ warn: false });
|
||||
});
|
||||
|
||||
it("warns about everything when nothing is trusted yet", () => {
|
||||
const v = linkVerdict("https://example.com/a", null, []);
|
||||
expect(v).toEqual({ warn: true, reason: "untrusted", domain: "example.com" });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user