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 g 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:
| Metric | Value |
|---|---|
| Precision | 97.0% |
| Recall | 90.9% |
| F1 score | 93.8% |
| False positive rate | 3.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.comcontains 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.