IDN Homograph Phishing Detector
A domain that swaps letters for nearly identical Unicode characters looks just like the original, so the person reading the email has no way to notice.
What it does: Analyzes a URL or an email when the person asks and says whether it is phishing, with the reasons and what to do. The security team sees each incident and how each signal voted.
Overview
Systems Engineering thesis at Universidad de San Buenaventura, co-authored with Juan Sebastián Fandiño Novoa.
A homograph domain swaps letters for nearly identical Unicode characters, like a Cyrillic "а" in place of a Latin "a". The system combines five signals into a risk score and explains every verdict. It is used from a browser extension and an Outlook add-in, which analyze the open email only when the person clicks Analyze, and from a dashboard for the university's security administrators.
The need
At a glance, a homograph domain is indistinguishable from the original. A filter that only says "dangerous" does not help anyone decide either: the person needs the reason and the next step. And the university's security team needs to see what came in and why it was flagged.
Constraints
- External reputation services can fail or lack API keys, and without them the system must not start flagging everything as phishing.
- These are people's emails: Colombia's Law 1581 of 2012 limits what is stored, and personal data cannot be sent to the LLM.
- The extension does not read the inbox in the background: it only analyzes the message the person opens and asks to analyze.
- The public datasets reviewed contained no real IDN homographs.
- The system advises; it does not block or quarantine email. A person handles the incident response.
The solution
An API that takes a URL, an email or an .eml file and returns a verdict with its risk score, the reasons and the steps to follow. It is used by an extension for Chrome, Edge and Firefox, an Outlook add-in and a dashboard where administrators see incidents, each agent's score and each user's permissions.
How it works
services/orchestrator.py gathers five signals and agents/fusion_agent.py computes the final risk. It deploys with Docker Compose on Coolify: backend, PostgreSQL, Redis, ChromaDB and Ollama for embeddings.
- Input: a URL, an email or an .eml file (
/api/v1/analyze,/analyze_email,/analyze_eml) - IDN agent: normalizes to NFC and Punycode, looks up confusables from the Unicode TR#39 catalog and measures similarity to the most visited domains with a BK-tree
- Threat intelligence: VirusTotal, URLScan.io, Google Safe Browsing and WhoisXML in parallel, cached in Redis for one hour
- LLM agent: analyzes the content through an OpenAI-compatible gateway, strips personal data before sending and adds similar cases retrieved from ChromaDB as context (RAG)
- URL classifier (a Hugging Face ONNX model) and a page probe with an SSRF-hardened client, plus the email's SPF and DKIM
- Fusion: PHISHING when risk reaches θ = 0.30; the incident and its reasons are stored in PostgreSQL
Technical decisions
- The θ = 0.30 threshold sits above the degradation floor (0.25): if external services fail and their signals fall back to neutral, the system doesn't flag everything as phishing. A lower threshold gave more recall in the evaluation but left that failure open (
docs/EVAL-T4-T7.md,core/constants.py). - The backend stopped depending on a local model (Ollama and LlamaStack) and calls a remote provider through an OpenAI-compatible gateway with retries and a fallback model (
core/llm_gateway.py). - Source IP, Message-ID and headers are encrypted with AES-256-GCM, and the email body is not stored by default (
STORE_EMAIL_CONTENT=false, Colombian Law 1581 of 2012).
Engineering challenges
- A full review on September 25, 2026 found 17 issues: sessions, the threat intelligence cache, the ChromaDB and PostgreSQL replicas, and the evaluation methodology. Each fix came with its own regression test (
docs/correcciones-revision-integral-2026-09-26.md). - Atomic refresh-token rotation in Redis: with five concurrent renewals of the same token, only one is accepted.
How it's validated
- pytest with unit, integration, system, regression, smoke, sanity and acceptance markers;
verify.shenforces 90% coverage. - Integration tests spin up ephemeral PostgreSQL and ChromaDB instead of using mocks.
- An adversarial suite for the agents and the RAG (
tests/security/): prompt injection, information disclosure, RAG poisoning and malicious output, following categories LLM01, LLM02, LLM04 and LLM05 of the OWASP Top 10 for LLM applications. - Last run documented in the repository (September 26, 2026): 972 tests passed and 28 skipped, with 92.6% coverage.
Limits & next steps
- The comparison against a monolithic classifier is being re-run: the review found duplicated URLs and cases shared between calibration and evaluation, so I won't publish those metrics until the new run is done.
- The public datasets we reviewed had no real IDN homographs; the homograph corpus used in the evaluation is synthetic.
- Without API keys, the threat intelligence signals default to zero.
My part
Thesis built by Juan Sebastián Fandiño Novoa and me. Together we built the backend described on this page, the browser extension with the Outlook add-in, the admin dashboard, and the local and lab infrastructure with Docker Compose and Kubernetes.
Stack
Interested in this kind of work?
I'm looking for remote work as a Full Stack Engineer. If you'd like to talk about this project or your team, get in touch.