A practical guide to QR code risks, real-world attacks, and safe scanning practices for developers, security researchers, and everyday users.
- What is QRishing?
- Attack Vectors
- Real-World Incidents
- QR Code Anatomy
- Safe Scanning Checklist
- For Developers
- Detection Techniques
- Tools & Resources
- Contributing
QRishing (QR + phishing) is an attack where malicious actors replace or overlay legitimate QR codes with ones that redirect users to fraudulent websites, trigger malware downloads, or steal credentials.
Unlike traditional phishing links, QR codes are opaque to the human eye β you cannot inspect the destination URL before scanning.
Legitimate QR Code Malicious QR Code
ββββββ ββββββ
ββ ββ ββ ββ
ββ ββ ββ β ββ ββ ββ
ββ ββ ββ ββ
ββββββ ββββββ
β https://bank.com β https://bank-login.ru
Attackers print malicious QR codes on stickers and place them over legitimate ones in public spaces β restaurants, parking meters, bike-sharing stations.
Documented locations: Restaurant tables, EV charging stations, parking payment terminals, public transit posters.
QR codes embedded in phishing emails to bypass URL scanners. Security filters check links, not images β a QR code in a PDF attachment is invisible to most scanners.
Used in Microsoft 365 credential theft campaigns. The attacker generates a real-time QR code that encodes a session-hijacking URL, bypassing MFA.
Fake IT department emails with QR codes claiming to be "multi-factor authentication setup" or "VPN reconfiguration" instructions.
QR codes encoding WIFI:T:WPA;S:<SSID>;P:<password>;; can silently connect a user's device to a rogue access point.
WIFI:T:WPA;S:FreeAirport_WiFi;P:;H:false;;
β No password β rogue hotspot
On mobile, QR codes can trigger deep links (myapp://action?token=...) that bypass browser security and directly invoke app actions.
| Year | Incident | Impact |
|---|---|---|
| 2022 | FBI warning on malicious QR codes at parking meters across the US | Nationwide alert issued |
| 2022 | FTX bankruptcy scammers replaced official QR codes in TV interviews | Thousands of dollars stolen |
| 2023 | Microsoft 365 QR phishing campaign targeting energy sector | 1,000+ corporate emails targeted |
| 2023 | QR codes in physical mail impersonating IRS and FedEx | High click-through rate due to perceived legitimacy |
| 2024 | EV charging station QR replacement attacks in UK and EU | Payment credential theft |
Understanding the structure helps developers build safer scanners.
βββββββββββββββββββββββββββββββββββ
β ββ ββ ββ ββ ββ ββ ββ ββ β β Quiet zone (4 modules)
β ββ βββββββββ βββββββββ ββ β
β ββ β β β β βββββββ β β β β ββ β β Finder patterns (3 corners)
β ββ β β β β β β β β ββ β
β ββ β β β β β β β β β β ββ β
β ββ βββββββββ βββββββββ ββ β
β ββ β Timing pattern β ββ β β Alignment + timing modules
β ββ [DATA CODEWORDS] ββ β β Encoded payload
β ββ βββββββββ ββ β
β ββ β β β β β Format info ββ β β Error correction level
β ββ β β β β ββ β
β ββ β β β β β ββ β
β ββ βββββββββ ββ β
βββββββββββββββββββββββββββββββββββ
Key facts:
- QR codes can encode up to 4,296 alphanumeric characters or 7,089 numeric digits
- Error correction levels: L (7%), M (15%), Q (25%), H (30%) β higher = more redundancy
- Version 1 (21Γ21) to Version 40 (177Γ177) modules
- The data payload is XOR-masked with one of 8 mask patterns to avoid visual patterns
- Preview the URL before opening β use a scanner that shows the decoded URL before navigating
- Check if the domain matches the expected organization
- Be suspicious of QR codes in unexpected emails, even from known senders
- Look for physical tampering β stickers placed over original QR codes
- Never scan QR codes that claim to "fix" a security problem
- On mobile, disable automatic link-following in your QR scanner
- Use tamper-evident materials for printed QR codes
- Add your domain/logo inside the QR code (logo embedding)
- Implement QR code monitoring (track scan events server-side)
- Include the destination URL in plain text alongside the QR code
- Rotate QR codes periodically for sensitive operations
Always validate the decoded URL before opening it. Never auto-navigate.
function isSafeUrl(decoded) {
try {
const url = new URL(decoded);
// Only allow http/https
if (!['http:', 'https:'].includes(url.protocol)) return false;
// Check against known malicious TLDs (example)
const suspiciousTLDs = ['.ru', '.cn', '.tk', '.ml', '.ga', '.cf'];
if (suspiciousTLDs.some(tld => url.hostname.endsWith(tld))) {
return 'warn'; // Warn, don't block
}
return true;
} catch {
return false; // Not a URL
}
}function detectQRType(data) {
if (/^https?:\/\//i.test(data)) return { type: 'URL', risk: 'medium' };
if (/^WIFI:/i.test(data)) return { type: 'WiFi', risk: 'high' };
if (/^mailto:/i.test(data)) return { type: 'Email', risk: 'medium' };
if (/^tel:/i.test(data)) return { type: 'Phone', risk: 'low' };
if (/^BEGIN:VCARD/i.test(data)) return { type: 'vCard', risk: 'low' };
if (/^smsto?:/i.test(data)) return { type: 'SMS', risk: 'medium' };
if (/^geo:/i.test(data)) return { type: 'Location', risk: 'low' };
if (/^[a-z][a-z0-9+\-.]*:\/\//i.test(data)) return { type: 'DeepLink', risk: 'high' };
return { type: 'Text', risk: 'low' };
}async function resolveRedirects(url, maxHops = 5) {
const chain = [url];
let current = url;
for (let i = 0; i < maxHops; i++) {
const res = await fetch(current, { method: 'HEAD', redirect: 'manual' });
if (res.status >= 300 && res.status < 400) {
current = res.headers.get('location');
chain.push(current);
} else break;
}
return chain; // Show the full redirect chain to the user
}<script src="https://cdn.jsdelivr.net/npm/jsqr/dist/jsQR.js"></script>
<script>
async function scanFromFile(file) {
const img = await createImageBitmap(file);
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const result = jsQR(imageData.data, canvas.width, canvas.height);
return result?.data ?? null;
}
</script>def score_qr_url(url: str) -> dict:
import re
from urllib.parse import urlparse
score = 0
flags = []
parsed = urlparse(url)
# Suspicious patterns
if re.search(r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', parsed.netloc):
score += 30; flags.append('IP address instead of domain')
if len(parsed.netloc) > 50:
score += 20; flags.append('Unusually long domain')
if parsed.netloc.count('.') > 4:
score += 15; flags.append('Excessive subdomains')
if re.search(r'(login|signin|verify|account|secure|update)', parsed.path, re.I):
score += 25; flags.append('Sensitive keyword in path')
if re.search(r'(paypal|amazon|microsoft|apple|google)', parsed.netloc, re.I):
if not parsed.netloc.endswith(('.paypal.com','.amazon.com','.microsoft.com')):
score += 40; flags.append('Brand name in non-official domain')
return {
'score': min(score, 100),
'risk': 'high' if score > 60 else 'medium' if score > 30 else 'low',
'flags': flags
}| Tool | Platform | Privacy | Notes |
|---|---|---|---|
| Qroole QR Code Reader | Chrome, Firefox, Edge | β 100% local | 4 scan modes, history, 10 languages |
| ZXing | Java/Android | β Local | Industry-standard library |
| jsQR | JavaScript | β Local | Pure JS, no dependencies |
| pyzbar | Python | β Local | Wraps ZBar library |
| QR Scanner (iOS) | iOS | β Local | Built into Camera app since iOS 11 |
| Library | Language | License |
|---|---|---|
| qrcode | Python | BSD |
| qrcode.js | JavaScript | MIT |
| QRCoder | C# | MIT |
| rust-qrcode | Rust | MIT/Apache |
- FBI PSA on QR Code Fraud (IC3)
- Microsoft QRishing Campaign Analysis (2023)
- ISO/IEC 18004:2015 β QR Code Standard
- OWASP Mobile Security β QR Code Risks
- Zetter, K. β "QR Codes Used in Phishing Attacks"
- Qroole QR Code Reader β scans camera, pages, uploaded images and right-click targets. All decoding happens locally, no data sent to servers. Available for Chrome, Firefox and Edge.
Contributions are welcome! Please:
- Fork this repository
- Add your resource, fix, or improvement
- Submit a pull request with a clear description
What we're looking for:
- New real-world QRishing incidents (with sources)
- Code examples in additional languages
- Detection techniques and heuristics
- Tool additions (must be open-source or privacy-respecting)
This work is dedicated to the public domain under CC0 1.0. You may copy, modify, and distribute without permission.
Made with β€οΈ by Qroole β Privacy-first QR tools for the web
