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.
Prerequisites
Before you begin, make sure you have the following ready:
- A computer running macOS, Linux, or Windows
- Terminal or command-line access
- Administrator or sudo privileges (for system-level changes)
- A stable internet connection for downloading tools
Step 1 - Understand 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.
Step 2 - How QR Codes Enable Attacks
A QR code simply encodes text, typically a 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.
Step 3 - 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
Step 4 - Build 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.
Step 5 - 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.
Step 6 - 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
Troubleshooting
Configuration changes not taking effect
Restart the relevant service or application after making changes. Some settings require a full system reboot. Verify the configuration file path is correct and the syntax is valid.
Permission denied errors
Run the command with sudo for system-level operations, or check that your user account has the necessary permissions. On macOS, you may need to grant terminal access in System Settings > Privacy & Security.
Connection or network-related failures
Check your internet connection and firewall settings. If using a VPN, try disconnecting temporarily to isolate the issue. Verify that the target server or service is accessible from your network.
Frequently Asked Questions
How long does it take to protect yourself from qr code phishing quishing?
For a straightforward setup, expect 30 minutes to 2 hours depending on your familiarity with the tools involved. Complex configurations with custom requirements may take longer. Having your credentials and environment ready before starting saves significant time.
What are the most common mistakes to avoid?
The most frequent issues are skipping prerequisite steps, using outdated package versions, and not reading error messages carefully. Follow the steps in order, verify each one works before moving on, and check the official documentation if something behaves unexpectedly.
Do I need prior experience to follow this guide?
Basic familiarity with the relevant tools and command line is helpful but not strictly required. Each step is explained with context. If you get stuck, the official documentation for each tool covers fundamentals that may fill in knowledge gaps.
Is this approach secure enough for production?
The patterns shown here follow standard practices, but production deployments need additional hardening. Add rate limiting, input validation, proper secret management, and monitoring before going live. Consider a security review if your application handles sensitive user data.
Where can I get help if I run into issues?
Start with the official documentation for each tool mentioned. Stack Overflow and GitHub Issues are good next steps for specific error messages. Community forums and Discord servers for the relevant tools often have active members who can help with setup problems.
Related Articles
- How To Protect Yourself From Sim Swap Attack Prevention
- Protect Yourself from Deepfake Identity Theft
- Async Code Review Process Without Zoom Calls
- Protect Yourself from Doxxing After Meeting Someone
- What Happens If You Click A Phishing Link On Chrome
- Does Cursor AI Store Your Code on Their Servers Data Built by theluckystrike. More at zovo.one