A Flask-based web application that analyses URLs through a seven-stage detection pipeline to classify them as Safe, Suspicious, or Malicious β and explains exactly why.
PhishGuard analyses every URL through a multi-stage pipeline β from raw input all the way to a human-readable safety recommendation β so you know exactly why a link is flagged, not just that it was flagged.
It accepts input as:
Results include a full breakdown of URL structural features, contextual signals, weighted feature fusion scores, redirect chains, and a plain-English safety recommendation.
The flowchart below describes the full processing pipeline:
User Input (URL or QR Code)
β
βΌ
βββββββββββββββββββββββ
β Stage 1 β
β Input Detection β ββ URL text OR QR image β decoded URL
ββββββββββ¬βββββββββββββ
β
βΌ
βββββββββββββββββββββββ
β Stage 2 β
β Validate & β ββ Structural check, add scheme, lowercase host,
β Normalise β strip tracking params (utm_*, fbclid, gclidβ¦)
ββββββββββ¬βββββββββββββ
β
βΌ
βββββββββββββββββββββββ
β Stage 3 β
β Redirect β ββ Follow up to 5 HTTP hops to find final destination
β Resolution β Records full redirect chain
ββββββββββ¬βββββββββββββ
β
βΌ
βββββββββββββββββββββββ
β Stage 4 β ββ Domain entropy, suspicious TLDs, IP-address hosts,
β URL Feature β brand in subdomain, Punycode/IDN, URL length,
β Extraction β query param count, executable extensions, keywordsβ¦
ββββββββββ¬βββββββββββββ
β
βΌ
βββββββββββββββββββββββ
β Stage 5 β ββ Cross-domain redirect chains, brand impersonation
β Contextual β (e.g. "paypal" in URL but domain β paypal.com),
β Analysis β credential-harvest paths, HTTP on sensitive domains
ββββββββββ¬βββββββββββββ
β
βΌ
βββββββββββββββββββββββ
β Stage 6 β ββ Weighted combination:
β Feature Fusion β 55% URL features + 45% contextual signals
β & Risk Scoring β Hard-boost rules for definitive signals
ββββββββββ¬βββββββββββββ
β
βΌ
βββββββββββββββββββββββ
β Stage 7 β ββ Safe (<35%) / Suspicious (35β65%) / Malicious (>65%)
β Classification & β Plain-English safety recommendation with per-signal
β Recommendation β explanations
βββββββββββββββββββββββ
| # | Stage | What it does |
|---|---|---|
| 1 | Input Detection | Accepts a typed URL or a QR image. When an image is uploaded it is decoded via pyzbar to extract the embedded URL. |
| 2 | Validate & Normalise | Checks structural validity. Adds https:// if missing, lowercases the host, removes default ports, strips common tracking parameters (utm_*, fbclid, gclid, msclkid, etc.). |
| 3 | Redirect Resolution | Follows HTTP redirects (up to 5 hops) using a HEAD request with a browser-like User-Agent to find the true destination. The full redirect chain is stored and displayed. |
| 4 | URL Feature Extraction | Extracts 15+ structural features: domain entropy, suspicious TLDs, raw-IP hosts, brand keywords in subdomain, Punycode/IDN homograph detection, URL length, query-parameter count, executable file extensions, hex-encoded characters, redirect patterns in path, and more. |
| 5 | Contextual Analysis | Layers contextual signals on top: cross-domain redirect chains, brand impersonation detection (claimed brand vs. actual domain), credential-harvesting path patterns (/login, /verify, /account, etc.), and unencrypted HTTP on sensitive domains. |
| 6 | Feature Fusion & Risk Scoring | Combines URL structural features (55% weight) and contextual signals (45% weight) into a single fused score. Hard-boost rules override the weighted score for definitive indicators (IP-based hosts, confirmed brand impersonation, executable links). |
| 7 | Safety Recommendation | Maps the fused score to a classification and generates a plain-English recommendation, augmented with specific notes for each triggered signal (shortener detected, IP host, brand mismatch, executable link, etc.). |
| Verdict | Score Range | Meaning |
|---|---|---|
| β Safe | < 35% | No significant phishing indicators. Proceed with normal caution. |
| β οΈ Suspicious | 35% β 65% | Risk signals present. Verify independently before entering credentials. |
| π¨ Malicious | > 65% | Strong phishing / malware indicators. Avoid this link entirely. |
These bypass weighted scoring and force the fused score to a minimum threshold:
| Signal | Minimum fused score |
|---|---|
| Raw IP address as host | 60% |
| Brand name in subdomain | 65% |
| Confirmed brand impersonation | 72% |
Executable file extension (.exe, .bat, etc.) |
78% |
| Trusted domain (no impersonation) | capped at 25% |
| Layer | Technology |
|---|---|
| Web framework | Flask 3.0.3 |
| ORM / Database | Flask-SQLAlchemy 3.1.1 + SQLite |
| Authentication | Flask-Login 0.6.3 |
| CSRF protection | Flask-WTF 1.2.1 |
| Password hashing | Werkzeug 3.0.3 |
| HTTP / redirect resolver | requests 2.32.3 |
| QR decoding | pyzbar 0.1.9 + Pillow 10.4.0 |
| Environment config | python-dotenv 1.0.1 |
| Frontend | Vanilla JS + custom CSS design system |
Internship-26/
βββ app/
β βββ __init__.py # Application factory β extensions + blueprint registration
β βββ auth.py # Authentication blueprint (login / register / logout)
β βββ models.py # SQLAlchemy models: User, ScanHistory
β βββ routes.py # Main blueprint (about page)
β βββ scanner.py # Core pipeline β all 7 stages + Flask routes
β βββ static/
β β βββ css/style.css # Full design system + pipeline UI components
β β βββ js/main.js # Alerts, nav toggle, password show/hide, count-up
β βββ templates/
β βββ base.html # Master layout with navbar, flash messages, footer
β βββ about.html # Pipeline documentation page
β βββ auth/
β β βββ login.html
β β βββ register.html
β βββ scanner/
β βββ dashboard.html # Stats cards + recent scans
β βββ scan.html # Tabbed URL / QR input form
β βββ result.html # Full analysis breakdown
β βββ history.html # Full scan history list
βββ instance/
β βββ app.db # SQLite database (auto-created)
βββ config.py # Config classes (Development / Testing / Production)
βββ run.py # Entry point β creates tables, seeds demo user, starts server
βββ requirements.txt
βββ .env # Environment variables (not committed)
User
id, username (unique), email (unique), password_hash,
created_at, is_active
β relationship: scans (ScanHistory)
ScanHistory
id, user_id (FK), url, original_url, input_type (url|qr),
prediction (safe|suspicious|malicious), probability (0.0β1.0),
risk_level (LOW|MEDIUM|HIGH), model_version,
recommendation (text), details_json (JSON blob),
scanned_at
The details_json column stores the full pipeline breakdown:
{
"url_features": { "domain": "...", "is_ip_address": false, ... },
"context": { "brand_mismatch": true, "num_redirects": 2, ... },
"fusion": { "fused_score": 0.74, "url_score": 0.68, ... },
"redirect_chain": ["https://bit.ly/xxx", "https://evil.example.com"]
}
git clone <repo-url>
cd Internship-26
python -m venv venv
# Windows
venv\Scripts\activate
# macOS / Linux
source venv/bin/activate
pip install -r requirements.txt
Note β QR code support:
pyzbarrequires thezbarshared library.
- Windows: the
pyzbarwheel bundleslibzbarautomatically β no extra step needed.- macOS:
brew install zbar- Linux (Debian/Ubuntu):
sudo apt-get install libzbar0
Copy .env.example to .env and set your values:
FLASK_APP=run.py
FLASK_ENV=development
SECRET_KEY=your-secret-key-here
DATABASE_URL=sqlite:///app.db
# Optional: point at a real ML detection service
# SCAN_API_URL=https://your-model-service.example.com/predict
python run.py
On first run this will:
demo@phishguard.io / demo1234http://localhost:5000http://localhost:5000| URL | Expected result |
|---|---|
https://github.com |
β Safe |
https://secure-paypal-verify.xyz/login |
π¨ Malicious |
http://192.168.1.1/paypal-login-verify.exe |
π¨ Malicious |
https://login-amazon-account-update.tk |
π¨ Malicious |
https://en.wikipedia.org/wiki/Phishing |
β Safe |
POST /api/scanScan a URL via JSON. CSRF-exempt β suitable for programmatic use.
Request
POST /api/scan
Content-Type: application/json
{ "url": "https://example.com/path" }
Response 200 OK
{
"url": "https://example.com/path",
"original_url": "https://example.com/path",
"prediction": "safe",
"probability": 0.08,
"risk_level": "LOW",
"model_version": "pipeline-v2",
"recommendation": "This URL appears safe to visit...",
"url_features": {
"domain": "example.com",
"tld": ".com",
"is_ip_address": false,
"is_trusted_domain": false,
"has_suspicious_tld": false,
"has_brand_in_subdomain": false,
"url_length": 27,
"domain_entropy": 2.807,
"suspicious_keywords": [],
"url_feature_score": 0.07
},
"context": {
"num_redirects": 0,
"crosses_domains": false,
"brand_mismatch": false,
"credential_path": false,
"context_score": 0.0
},
"fusion": {
"fused_score": 0.08,
"url_weight": 0.55,
"context_weight": 0.45,
"url_score": 0.07,
"context_score": 0.0
},
"redirect_chain": []
}
Error responses
| Status | Body | Reason |
|---|---|---|
400 |
{"error": "url field is required"} |
Missing url field |
422 |
{"error": "invalid url"} |
URL fails structural validation |
| Variable | Default | Description |
|---|---|---|
SECRET_KEY |
change-me-in-production |
Flask session signing key |
DATABASE_URL |
sqlite:///app.db |
SQLAlchemy database URI |
SCAN_API_URL |
(empty) | External ML model endpoint. When empty, the built-in pipeline is used. |
FLASK_ENV |
development |
Enables debug mode when set to development |
PORT |
5000 |
Port the server listens on |
When SCAN_API_URL is set, the app POSTs {"url": "<url>"} to that endpoint and expects a JSON response with at minimum prediction, probability, and risk_level fields.
| Field | Value |
|---|---|
demo@phishguard.io |
|
| Password | demo1234 |
Created automatically on first run. Change or remove in run.py β seed_demo_user() before deploying.