Last updated: March 15, 2026

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:

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:

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:

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:

  1. Document the source: Note where the QR code was found, whether physical or digital
  2. Analyze the URL: Use tools like the Python script above to examine the destination
  3. Report to authorities: File reports with relevantCERT teams (US-CERT, CISA, or local equivalents)
  4. Notify affected users: If the QR code reached multiple people, warn potential victims
  5. 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