FoilGuard Detection Architecture — Multi-Signal Scoring and False Positive Analysis

A technical walkthrough of how FoilGuard detects domain impersonation. Covers the signal architecture, scoring weights, benchmark results against 60+ labeled domains, and the false positive tradeoffs that shaped design decisions.

Why blocklists are not enough

The canonical approach to phishing protection is blocklisting: maintain a list of known bad domains and block them. Google Safe Browsing, uBlock Origin, and most commercial products work this way. The problem is coverage lag. A phishing domain registered this morning and used this afternoon will not appear in any blocklist for hours or days — which is exactly the window attackers need.

Typosquatting campaigns are particularly well-suited to exploiting this lag. An attacker registers paypa1.com, runs a credential-harvesting campaign for 6 hours, and abandons the domain before any blocklist catches it. The infrastructure is cheap and disposable. The attacker rotates through dozens of variations.

FoilGuard's design premise: the decision to block should not depend on whether a specific domain has been seen before. It should depend on whether the domain looks like an impersonation of a known brand — a determination that can be made in under a millisecond using only the domain name itself.

Signal architecture

FoilGuard computes a risk score from 0–100 by running each domain through six independent signal detectors. Each detector returns a score contribution; contributions are summed and clamped to 100.

1. Levenshtein distance (typosquatting)

The engine compares the registrable domain against a list of ~200 high-value brand names using weighted Levenshtein distance. The score scales with proximity: distance 1 (single edit) contributes 55 points, distance 2 contributes 35 points, distance 3 contributes 15 points. Distance ≥ 4 does not contribute.

Computational cost is the primary constraint here. A naïve O(nm) Levenshtein computation against 200 brands adds up. FoilGuard applies a length filter first — brands whose length differs by more than 3 characters from the target are skipped — reducing the comparison set by ~70% on average.

2. Homoglyph normalisation

Before Levenshtein comparison, the domain is normalised: all visually confusable characters are mapped to their ASCII equivalents. Cyrillic а (U+0430) becomes a. Greek ο (U+03BF) becomes o. Fullwidth becomes g. Punycode domains (xn-- prefix) are decoded to Unicode before normalisation. If a domain scores 0 on raw Levenshtein but >0 after homoglyph normalisation, the normalised score is applied instead.

Digit substitution is handled as a special case within normalisation: 0→o, 1→l, 3→e, 4→a, 5→s, 7→t. The mapping is directional (digit → letter, not letter → digit) to avoid false-matching legitimate domains that happen to contain numbers.

3. Combosquatting detection

Combosquatting appends or prepends keywords to real brand names: paypal-secure.com, amazon-login.com, signin-google.com. The signal checks whether any known brand name appears as a substring of the registrable domain. If found, the surrounding context is examined for deceptive keywords — login, secure, account, verify, update, billing, support — which contribute an additional 20 points on top of the base brand-match score.

The key design decision here was the substring threshold. A domain like shopify-payments.com contains shopify and the word payments, which is a legitimate Shopify product name. The detector uses a minimum brand length of 5 characters and requires that the brand not be followed by another brand name (ruling out microsoftgoogle.com as a false positive in a different way).

4. Subdomain abuse

Legitimate services are accessed via their own domain. Phishing pages are sometimes hosted as subdomains of attacker-controlled domains: paypal.com.evil.xyz. The signal flags any hostname where a known brand name appears in a subdomain label but not in the registrable domain itself. Score: 70 points (high confidence).

5. Redirect chain length

The browser's navigation history is inspected for the number of automatic redirects that preceded the current navigation. Three or more rapid redirects indicate potential redirect chain abuse — a technique used to hide the final phishing destination from email link scanners. Score: 20 points per redirect beyond the second, capped at 40.

6. HTTP on a brand-similar domain

If the domain scores ≥ 10 on other signals and is being served over plain HTTP, an additional 25 points are added. Legitimate branded services universally use HTTPS. A brand-similar domain that insists on HTTP is almost certainly not the real service.

Benchmark results

The benchmark script (scripts/benchmark.mjs) tests the engine against 33 labeled phishing domains and 33 legitimate domains using only the synchronous detectors (no RDAP network call). At the default threshold of 65:

MetricValue
Precision97.0%
Recall90.9%
F1 score93.8%
False positive rate3.0%

The three false negatives at threshold 65 are all combosquatting variants where the deceptive keyword list does not include the specific word used. The single false positive is a domain that contains a brand substring but is a legitimate product. Both failure modes are addressable: expanding keyword and brand lists incrementally improves recall without affecting precision.

With RDAP enabled (the async path), recently-registered domains score an additional 20–40 points, pushing recall to approximately 95%+ for the most dangerous category of attacks — newly registered typosquats.

Threshold selection

The default threshold of 65 was chosen to sit in the gap between the highest-scoring false positive in the legitimate dataset and the lowest-scoring true positive in the phishing dataset. At 65, the two populations do not overlap in the current test data. The options page lets users adjust from 40 (strict) to 90 (permissive) based on their tolerance for false positives versus missed detections.

The tradeoff is not symmetric. A false negative (missed phishing page) costs the user a credential. A false positive (blocked legitimate site) costs the user a navigation and requires an override click. For most users in the target audience — individuals who have already opted in to additional security tooling — a slightly stricter default is preferable.

What the engine does not detect

FoilGuard does not inspect page content, certificates, or registrant data beyond domain age. It cannot detect:

  • Phishing hosted on legitimate infrastructure (e.g. Cloudflare Pages, GitHub Pages, Vercel) — the domain scores 0 because the registrable domain is known-good.
  • Newly coined brand names — a phishing domain impersonating a brand that isn't in the top-200 list will not trigger typosquatting detection.
  • Semantic deception — a domain like important-notice.com contains no brand name and scores 0.

These are genuine limitations, not implementation gaps. The first requires page-content analysis (possible but expensive). The second requires a much larger brand list (in progress). The third requires intent inference, which is a different class of problem.

Source and reproducibility

The full benchmark script, labeled dataset, and detection source are in the FoilGuard repository ↗. Run node scripts/benchmark.mjs --verbose to reproduce the numbers above. The sync-only path takes under 100 ms for the full dataset on a modern laptop.