ML-powered phishing URL detection using 30 heuristic features and a Random Forest classifier.
A URL is analysed across three layers:
- URL checks — pure regex and string analysis (IP address, length, shortener, symbols, port)
- Network checks — DNS resolution, WHOIS registration data, HTTP fetch, SSL state
- Content checks — HTML structure, form handlers, JavaScript patterns, redirect chains
30 features are extracted and passed to a Random Forest classifier trained on the UCI Phishing Websites dataset. The model returns a prediction (phishing / legitimate) with a confidence score.
phishing-detector/
├── app.py # Flask app factory + API routes
├── config.py # Dev / Test / Production config classes
├── scripts/
│ └── train_model.py # CLI trainer
├── src/
│ ├── features/
│ │ ├── extractor.py # Orchestrates all feature modules
│ │ ├── url_checks.py # 9 pure URL features
│ │ ├── network_checks.py # 8 network features
│ │ └── content_checks.py # 13 HTML/JS content features
│ ├── model/
│ │ ├── trainer.py # Train, evaluate, save
│ │ ├── predictor.py # Load, predict, cache
│ │ └── schemas.py # Pydantic request/response models
│ └── utils/
│ ├── logger.py # Structured text/JSON logging
│ └── exceptions.py # Typed exception hierarchy
├── static/
│ ├── css/main.css
│ └── js/app.js
├── templates/
│ └── index.html
├── Dockerfile
├── docker-compose.yml
└── Makefile
git clone https://github.com/bavasumit/phishing-detector.git
cd phishing-detector
python -m venv .venv && source .venv/bin/activate
pip install -r requirements-dev.txtcp .env.example .env
# Edit .env — set SECRET_KEY at minimummake train# Development
make dev
# Production-like (gunicorn)
make runOpen http://localhost:5000 in your browser.
# Build and start
make build
make up
# Tail logs
make logs
# Stop
make downThe Docker image trains the model at build time so the container starts ready to serve predictions.
Request
{ "url": "https://example.com" }Response — legitimate
{
"request_id": "a1b2c3d4",
"url": "https://example.com",
"domain": "example.com",
"prediction": "legitimate",
"confidence": 0.94,
"is_phishing": false,
"extraction_time_ms": 1823.4,
"features": {
"having_IP_Address": 1,
"URL_Length": 1,
"SSLfinal_State": 1,
"age_of_domain": 1
},
"warnings": []
}Response — phishing
{
"request_id": "e5f6g7h8",
"url": "http://192.168.1.1/paypal-login",
"domain": "192.168.1.1",
"prediction": "phishing",
"confidence": 0.97,
"is_phishing": true,
"extraction_time_ms": 2104.1,
"features": {
"having_IP_Address": -1,
"SSLfinal_State": -1,
"age_of_domain": -1,
"DNSRecord": -1
},
"warnings": ["WHOIS failed for 192.168.1.1"]
}Error responses
| Code | Meaning |
|---|---|
| 400 | Missing or malformed request body |
| 422 | Feature extraction failed |
| 503 | Model not loaded |
| 500 | Unexpected server error |
{ "status": "ok", "model_loaded": true, "environment": "production" }| # | Feature | Signal |
|---|---|---|
| 1 | having_IP_Address | Host is a raw IP |
| 2 | URL_Length | > 75 chars is suspicious |
| 3 | Shortening_Service | bit.ly, tinyurl, etc. |
| 4 | having_At_Symbol | @ hides real destination |
| 5 | double_slash_redirecting | // after scheme |
| 6 | Prefix_Suffix | Hyphen in domain name |
| 7 | having_Sub_Domain | Deep subdomain nesting |
| 8 | HTTPS_token | "https" in domain string |
| 9 | port | Non-standard port |
| 10 | SSLfinal_State | HTTPS + page loads |
| 11 | Domain_registration_length | Expiry > 1 year |
| 12 | Favicon | Loads from same domain |
| 13 | Request_URL | Ratio of external resources |
| 14 | Abnormal_URL | WHOIS domain mismatch |
| 15 | age_of_domain | Domain > 6 months old |
| 16 | DNSRecord | Domain resolves in DNS |
| 17 | Statistical_report | IP/TLD in blocklist |
| 18 | URL_of_Anchor | Off-domain anchor ratio |
| 19 | Links_in_tags | External script/link ratio |
| 20 | SFH | Form action target |
| 21 | Submitting_to_email | mailto: form handler |
| 22 | Redirect | HTTP redirect hop count |
| 23 | on_mouseover | Status bar spoofing |
| 24 | RightClick | Right-click suppressed |
| 25 | popUpWindow | window.open() on load |
| 26 | Iframe | Hidden iframe present |
| 27 | web_traffic | Domain popularity proxy |
| 28 | Page_Rank | Domain authority proxy |
| 29 | Google_Index | HTTPS + loads heuristic |
| 30 | Links_pointing_to_page | Anchor count in source |
Feature values: 1 = legitimate signal, 0 = unknown/neutral, -1 = phishing signal.
All values are set via environment variables. See .env.example for the full list.
| Variable | Default | Description |
|---|---|---|
FLASK_ENV |
development |
development, testing, production |
SECRET_KEY |
auto-generated | Required in production |
DATASET_PATH |
dataset.csv |
Training data path |
MODEL_PATH |
phishing_model.joblib |
Saved model path |
REQUEST_TIMEOUT |
8 |
Network call timeout in seconds |
MAX_URL_LENGTH |
2048 |
Maximum accepted URL length |
LOG_LEVEL |
INFO |
DEBUG, INFO, WARNING, ERROR |
LOG_FORMAT |
text |
text (dev) or json (prod) |
- Flask — web framework
- scikit-learn — Random Forest classifier
- Pydantic — request/response validation
- python-whois — WHOIS lookups
- BeautifulSoup4 — HTML parsing
- UCI Phishing Websites Dataset — training data

