cd /news/machine-learning/building-an-ai-powered-phishing-url-… · home topics machine-learning article
[ARTICLE · art-83616] src=dev.to ↗ pub= topic=machine-learning verified=true sentiment=· neutral

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.

read5 min views1 publishedAug 2, 2026

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 telllogin

, verify

, account

, secure

, update

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:

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:

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 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.

── more in #machine-learning 4 stories · sorted by recency
── more on @phishtank 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/building-an-ai-power…] indexed:0 read:5min 2026-08-02 ·