Building an AI-powered phishing URL detector A developer detailed the construction of an AI-powered phishing URL detector using Python, combining feature engineering, a gradient boosting classifier, and a language model escalation layer for ambiguous cases. The system analyzes structural patterns such as domain entropy, keyword presence, and subdomain counts to flag malicious URLs before user clicks. Phishing remains one of the most effective attack vectors in 2026 — not because defenders are incompetent, but because attackers have gotten very good at making malicious URLs look legitimate. A URL like secure-login.paypal-account-verification.com/oauth passes a casual glance. Your users won't catch it. A well-built detector can. This post walks through building a practical phishing URL detector in Python: feature engineering, training a classifier, and adding a language model escalation layer for ambiguous cases. Most phishing URLs share structural patterns that legitimate URLs avoid. The domain has an unusual character distribution. The path is unusually long. The hostname contains brand names paypal , netflix , google in odd positions — a subdomain or path component rather than the registrable domain itself. These signals aren't perfect individually, but they compose well into a classifier. The goal isn't 100% accuracy — it's adding a reliable, low-latency layer to your security stack that flags URLs worth inspecting before a user clicks. Before touching any model, decide what you're measuring. Here are the features that consistently show up in practice: mail.secure.paypal.verification.com has more dots than paypal.com http://192.168.1.1/login is a classic tell login , verify , account , secure , update python import re import math from urllib.parse import urlparse from collections import Counter SUSPICIOUS KEYWORDS = {"login", "verify", "account", "secure", "update", "confirm", "bank"} URL SHORTENERS = {"bit.ly", "tinyurl.com", "t.co", "goo.gl", "ow.ly"} def shannon entropy s: str - float: if not s: return 0.0 counts = Counter s total = len s return -sum c / total math.log2 c / total for c in counts.values def extract features url: str - dict: parsed = urlparse url if url.startswith "http" else f"https://{url}" hostname = parsed.hostname or "" path = parsed.path or "" full = url.lower digits = sum c.isdigit for c in hostname return { "url length": len url , "hostname length": len hostname , "dot count": hostname.count "." , "digit ratio": digits / len hostname if hostname else 0, "has ip": bool re.fullmatch r"\d{1,3} \.\d{1,3} {3}", hostname , "is shortener": hostname in URL SHORTENERS, "keyword count": sum kw in full for kw in SUSPICIOUS KEYWORDS , "entropy": shannon entropy hostname , "path length": len path , "has at sign": "@" in url, "subdomain count": max 0, len hostname.split "." - 2 , "uses https": parsed.scheme == "https", } Run this against a few URLs to sanity-check — paypal.com should score very differently from secure-paypal-login.verification-center.com . A gradient boosting model scikit-learn's HistGradientBoostingClassifier works well here: it handles the mix of binary and continuous features, trains fast, and is interpretable enough that you can explain why it flagged a URL. For training data, the PhishTank dataset phishing URLs and the Tranco top-1M legitimate URLs are the standard sources. Here's the training scaffold: python import pandas as pd from sklearn.ensemble import HistGradientBoostingClassifier from sklearn.model selection import train test split from sklearn.metrics import classification report import joblib def load dataset phishing path: str, legit path: str - tuple pd.DataFrame, pd.Series : phishing urls = pd.read csv phishing path, names= "url" "url" .dropna .tolist legit urls = pd.read csv legit path, names= "url" "url" .dropna .tolist records = for url in phishing urls: feats = extract features url feats "label" = 1 records.append feats for url in legit urls: feats = extract features url feats "label" = 0 records.append feats df = pd.DataFrame records return df.drop "label", axis=1 , df "label" def train phishing path: str, legit path: str, model path: str = "phishing model.joblib" : X, y = load dataset phishing path, legit path X train, X test, y train, y test = train test split X, y, test size=0.2, random state=42 clf = HistGradientBoostingClassifier max iter=200, learning rate=0.05, max depth=6 clf.fit X train, y train print classification report y test, clf.predict X test joblib.dump clf, model path print f"Model saved to {model path}" On a balanced dataset of ~100k URLs, this typically reaches 96-98% accuracy with a false positive rate under 2%. That is a reasonable starting point. The classifier is fast and cheap — it should be your first filter. But for borderline cases score between 0.3 and 0.7 , you can escalate to a language model for contextual analysis. An LLM can reason about things the feature extractor cannot: does the domain plausibly belong to the brand it is impersonating? Does the path structure match known phishing kit patterns? The key is structured output — a verdict and a reasoning string, not free-form text: php import json def build prompt url: str - str: return "You are a security analyst. Analyze this URL for phishing indicators.\n" f"URL: {url}\n\n" "Return JSON with these fields:\n" ' verdict: "phishing" | "legitimate" | "suspicious"\n' " confidence: float 0.0-1.0\n" " reasoning: one sentence\n" " indicators: list of strings\n" "Return only valid JSON, no other text." def llm assess url url: str, client - dict: response = client.chat build prompt url , max tokens=256 try: return json.loads response.text except json.JSONDecodeError: return {"verdict": "suspicious", "confidence": 0.5, "reasoning": "parse error", "indicators": } def classify url url: str, clf, client, threshold low=0.3, threshold high=0.7 - dict: features = extract features url feature df = pd.DataFrame features score = float clf.predict proba feature df 0 1 if score = threshold high: return {"verdict": "phishing", "score": score, "method": "classifier"} elif score <= threshold low: return {"verdict": "legitimate", "score": score, "method": "classifier"} else: result = llm assess url url, client result "score" = score result "method" = "llm escalation" return result This hybrid approach keeps costs low — the language model only sees the roughly 20% of URLs that land in the ambiguous middle band. A few things matter when you move from notebook to production: Latency : the feature extractor runs in microseconds. The classifier adds a few milliseconds. Reserve LLM calls for borderline cases and set a hard timeout 500ms is reasonable for a synchronous security check . Domain age : WHOIS lookups add latency but are worth it for high-confidence verdicts. Domains registered in the last 30 days are disproportionately malicious. Cache results aggressively. Feedback loop : every false positive costs user trust. Build a reporting endpoint from day one and use the collected corrections to retrain monthly. What the model will not catch : homograph attacks, zero-day phishing kits not yet in training data, and legitimate services that just happen to look suspicious. A threat intelligence feed — checking URLs against known-bad lists — fills the gaps a classifier alone cannot cover. See our security hardening checklists https://ayinedjimi-consultants.fr/checklists for a structured reference of additional defensive layers worth stacking alongside URL detection. Phishing URL detection is a well-scoped problem with measurable real-world impact. A feature-based gradient boosting classifier gets you to ~97% accuracy with minimal infrastructure. Adding a language model escalation layer handles the ambiguous cases that sit on the decision boundary. Together they make a solid, explainable, cheap-to-run first line of defense. Start with the feature extractor. Train on PhishTank + Tranco. Measure your false positive rate against your own traffic before deploying in blocking mode. Iterate from there. I run AYI NEDJIMI Consultants, a cybersecurity consulting firm. We publish free security hardening checklists — PDF and Excel.