QR codes have become ubiquitous in modern workflows—payments, authentication, product tracking, and event check-ins all rely on them. However, this convenience has attracted threat actors who exploit QR codes for phishing attacks, a technique called “quishing.” This guide provides developers and power users with practical strategies to identify, prevent, and respond to QR code phishing attempts.
Understanding Quishing Attacks
Quishing combines “QR code” with “phishing.” Attackers embed malicious URLs in QR codes that redirect users to credential-harvesting pages, malware download sites, or fake login portals. The attack succeeds because QR codes bypass traditional email filters and appear innocuous when printed on physical materials.
Attack vectors include:
- Physical placement: Attackers replace legitimate QR codes on posters, flyers, or product packaging with malicious ones
- Email embedding: QR codes embedded in emails bypass many spam filters since they contain images rather than clickable links
- URL shortening abuse: Attackers use shortened URLs within QR codes to disguise the true destination
For developers building applications that generate or scan QR codes, understanding these attack surfaces is essential for building secure systems.
How QR Codes Enable Attacks
A QR code simply encodes text—typically an URL. When scanned, the phone’s camera or a dedicated app interprets this text and acts on it. The critical security gap: users cannot determine a QR code’s destination without scanning it first.
Consider this example of a QR code that appears legitimate but leads to a malicious site:
# A simple Python script to generate a QR code
import qrcode
# This QR code looks innocent but points to attacker-controlled domain
malicious_data = "https://example-login.com.phishing-website.xyz/reset-password"
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(malicious_data)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
img.save("qr_code.png")
The URL uses a technique called “typosquatting”—replacing legitimate characters with similar-looking ones to deceive users.
Detecting Malicious QR Codes
Several techniques help identify potentially dangerous QR codes before scanning:
URL Analysis
Before visiting any URL from a QR code, extract and analyze it:
from urllib.parse import urlparse
import qrcode
from PIL import Image
from pyzbar.pyzbar import decode
def extract_qr_url(image_path):
"""Extract URL from a QR code image."""
decoded_objects = decode(Image.open(image_path))
for obj in decoded_objects:
if obj.type == 'QRCODE':
return obj.data.decode('utf-8')
return None
def analyze_url(url):
"""Analyze URL for suspicious characteristics."""
parsed = urlparse(url)
warnings = []
# Check for URL shorteners
shorteners = ['bit.ly', 'tinyurl.com', 'goo.gl', 't.co', 'is.gd']
if any(shortener in parsed.netloc for shortener in shorteners):
warnings.append("URL uses a shortening service")
# Check for suspicious TLDs
suspicious_tlds = ['.xyz', '.top', '.gq', '.tk', '.ml', '.cf']
if any(parsed.netloc.endswith(tld) for tld in suspicious_tlds):
warnings.append("Unusual top-level domain")
# Check for excessive subdomains
if parsed.netloc.count('.') > 3:
warnings.append("Excessive subdomain depth")
# Check for IP address usage
import re
if re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', parsed.netloc):
warnings.append("URL uses IP address instead of domain")
return warnings
# Example usage
url = extract_qr_url("sample_qr.png")
if url:
warnings = analyze_url(url)
for warning in warnings:
print(f"Warning: {warning}")
Visual Inspection
Physical QR codes often show signs of tampering:
- Stickers placed over existing codes
- Different paper texture or print quality
- Codes in unusual locations (e.g., on parking meters, elevator buttons)
- Codes that are too large or poorly aligned
Building QR Security into Applications
For developers creating applications that handle QR codes, implement these defensive measures:
URL Preview Screens
Always show users the destination URL before opening it:
// React component for safe QR code navigation
import { useState } from 'react';
function QRNavigator({ extractedUrl }) {
const [showWarning, setShowWarning] = useState(false);
const [url, setUrl] = useState(extractedUrl);
const analyzeUrl = (urlString) => {
const warnings = [];
// Check for URL shorteners
const shorteners = ['bit.ly', 'tinyurl.com', 'goo.gl'];
if (shorteners.some(s => urlString.includes(s))) {
warnings.push('URL uses a shortening service');
}
// Check for IP addresses
if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(new URL(urlString).hostname)) {
warnings.push('Destination uses an IP address');
}
// Check for suspicious TLDs
const suspiciousTlds = ['.xyz', '.top', '.gq', '.tk'];
if (suspiciousTlds.some(tld => urlString.endsWith(tld))) {
warnings.push('Unusual domain extension');
}
return warnings;
};
const warnings = analyzeUrl(url);
return (
<div className="qr-navigator">
<p>Destination: <strong>{url}</strong></p>
{warnings.length > 0 ? (
<div className="warning-box">
<h3>⚠️ Security Warnings</h3>
<ul>
{warnings.map((w, i) => <li key={i}>{w}</li>)}
</ul>
<button onClick={() => window.open(url, '_blank')}>
Proceed Anyway
</button>
</div>
) : (
<button onClick={() => window.open(url, '_blank')}>
Open URL
</button>
)}
</div>
);
}
URL Unwrapping Services
For enterprise environments, implement URL unwrapping to reveal the true destination:
import requests
def unwrap_url(shortened_url, timeout=5):
"""Follow redirects to get the final URL."""
try:
response = requests.head(
shortened_url,
allow_redirects=True,
timeout=timeout,
headers={'User-Agent': 'SecurityScanner/1.0'}
)
return response.url
except requests.RequestException as e:
return f"Error: {str(e)}"
# Usage
final_url = unwrap_url("https://bit.ly/3xY7z8A")
print(f"Actual destination: {final_url}")
This technique reveals shortened URLs but introduces privacy considerations—ensure your implementation complies with applicable privacy regulations and organizational policies.
Protecting Your Organization
Beyond individual awareness, organizations should implement these measures:
Employee Training: Conduct regular security awareness sessions specifically covering quishing. Show examples of physical and digital quishing attempts targeting your industry.
Technical Controls: Deploy email security gateways that analyze QR codes within emails. Implement network-level URL filtering to block known malicious domains.
Reporting Mechanisms: Create clear channels for employees to report suspicious QR codes. Quick reporting enables faster response to emerging threats.
Physical Security: Regularly audit physical spaces for tampered QR codes. Include QR code inspection in security checklists for offices, retail locations, and public areas.
Responding to Quishing Incidents
If you or your organization encounters a quishing attempt:
- Document the source: Note where the QR code was found, whether physical or digital
- Analyze the URL: Use tools like the Python script above to examine the destination
- Report to authorities: File reports with relevantCERT teams (US-CERT, CISA, or local equivalents)
- Notify affected users: If the QR code reached multiple people, warn potential victims
- Block malicious domains: Add identified malicious URLs to blocklists
Related Reading
- How To Protect Yourself From Ai Voice Cloning Scam Calls
- Protect Yourself from Browser Extension Malware Installed
- Protect Yourself from Credential Stuffing Attack
- Protect Yourself from Deepfake Identity Theft
- Protect Yourself from Doxxing After Meeting Someone Through
Built by theluckystrike — More at zovo.one