Settings

A QR code is text, drawn

There is no code-signing, no allow-list, no notion of a trusted publisher. A symbol is a string with a four-bit header saying how the string was packed, and four ways of packing exist: numeric (0001), alphanumeric (0010), byte (0100) and kanji (1000).

Which one is used only changes the size. Numeric packs three digits into ten bits, so ten digits take 34; alphanumeric packs a pair of characters into eleven bits, but only from a 45-character set — the digits, capital A to Z, space, and $ % * + - . / :; byte mode takes anything at eight bits a character. Thirty digits fit a 21×21 symbol in numeric mode and need 29×29 in byte mode. The same effect shows in a URL: a fifty-character address in capitals encodes as a 29×29 symbol and the identical address in lower case needs 33×33, because lower case forces byte mode. This site's own encoder writes byte mode only, because it is the one that can represent everything.

None of that touches what the string means. The scanner decodes it, sees https:// at the front, and offers to open it. That gap is the whole problem: you cannot read a QR code, so you find out what it says by doing what it says.

Error correction, and the logo in the middle

Every symbol carries Reed-Solomon parity at one of four levels, recorded in the format information beside the top-left finder pattern so any scanner knows which. Higher correction spends more of the symbol on parity and less on payload: at the largest version, byte mode holds 2,953 bytes at L and 1,273 at H, so choosing H costs well over half the capacity.

What it buys is damage tolerance, and that is what lets a logo sit in the middle of a code. Encoding the same URL at each level, covering the centre with a white square and decoding with a standard reader, the largest patch that still read was:

LevelNominal recoverySymbolLargest centred patch still readable
L7%25×254×4 modules — 2.6% of the area
M15%29×298×8 modules — 7.6%
Q25%29×299×9 modules — 9.6%
H30%33×3313×13 modules — 15.5%

The same property works for the attacker. A code printed at H keeps scanning with a substantial patch stuck over it, so a partly-covered or tampered symbol does not announce itself by failing. "It scanned fine" is not evidence that nothing has been done to it.

What a code can carry besides a link

A URL is only the common case. The same square can hold:

  • WIFI:T:WPA;S:<network>;P:<password>;; — a network name and password. Scanning it offers to join, and a phone on a network someone else controls has handed them the position to watch or redirect unencrypted traffic.
  • tel: and smsto:<number>:<message> — a number dialled, or a message pre-written with its recipient and body already filled in, so all that is left is to press send.
  • mailto: with subject and body already set.
  • A vCard — BEGIN:VCARD, VERSION:3.0, then N, FN, ORG, TEL;TYPE=CELL, EMAIL, URL, ADR;TYPE=WORK — saved with whatever it says.
  • A deep link into an installed app, which opens straight into a screen inside it and skips whatever you would have read on the way there.

The attacks that actually work

They are unglamorous, which is why they work.

A sticker. Printed codes live in public on parking meters, restaurant tables, charging points and posters. A replacement sticker over the original costs nothing, needs no special access, and leaves the surrounding sign untouched and reassuring.

A domain that is close enough. Phones truncate. A preview showing the first forty characters of a long address is showing you the start, and the start is the part an attacker controls most freely: a real brand name can be a subdomain of something else entirely, and everything before an @ in a URL is a username, not the host.

A shortener. A shortened link resolves to a domain that tells you nothing, and its destination can be changed after the stickers are printed.

What to check before you tap

Look at the physical code first. A sticker over a printed sign, a slightly wrong colour, a raised edge or a code that does not match the branding around it is the cheapest signal available and the one nobody uses.

Then read the address, from the right rather than the left. The registered domain is the part immediately before the first single slash; everything to the left of it is subdomain and can say anything. Watch for a raw IP address in place of a name, an xn-- prefix (punycode, which can render as a lookalike), an unusually long chain of labels, and any @ in the address.

And never enter a payment detail or a password on a page you reached by scanning something in the street. Open the site yourself, from a bookmark or a search, and do it there.

What to use

QRSHIELD decodes a photo or a screenshot of a code instead of a live camera, so you read the text and decide for yourself rather than being offered a button. It classifies what came out — link, Wi-Fi, phone number, contact card — and flags plain http, an @ hiding the real host, a raw IP, fifteen known shorteners, punycode, long subdomain chains and login-shaped paths. Decoding uses the reader built into the browser, so nothing is downloaded and the address is never requested.

If the address survives that, SANILINK strips the tracking parameters off it before you open it, and PHISHLENS takes a fuller look at a suspicious link or the message it arrived in.

What this cannot tell you

Decoding tells you where a code points. It does not tell you whether that destination is safe. An ordinary domain with no warning signs can be a site compromised last week, and QRSHIELD says "no warning signs" rather than "safe" for that reason.

One practical limit is worth knowing before you rely on it. QRSHIELD uses the browser's own barcode reader rather than downloading a library, and that reader only exists where the operating system supplies one: Chrome and Edge on Android, macOS and ChromeOS. On Windows and Linux desktop, and in Firefox and Safari, there is nothing there to use, and the page says so instead of failing quietly.

What this is, and what it is not

The numbers came from real symbols. The mode indicators, the 45-character alphanumeric set, the version 40 byte capacities (2,953 at L down to 1,273 at H) and the patch sizes in the table were all measured by encoding and decoding actual codes, not quoted from memory.

High error correction cuts both ways. It is what lets a logo sit over the middle of a code, and it is what lets a partly-covered or altered code keep scanning without complaint.

Decoding is not the same as visiting. QRSHIELD turns the symbol into text and stops. It never requests the address, so nothing on the other end learns that you looked.

A QR code has no signature and never will. The format carries no notion of who made a code. Any claim about who a code belongs to comes from where it is stuck, and a sticker is not evidence.

Questions people ask

Can a QR code contain a virus?

Not in the sense of carrying executable code: a symbol holds text and nothing else. What it can do is take you somewhere that asks you to install something, or hand a browser a deep link into an app. The payload is the destination, not the square.

How do I see where a code goes without opening it?

Photograph or screenshot it and decode the image rather than pointing a live camera at it. Decoding gives you the text, and you read the domain and decide. QRSHIELD does that in the browser and never requests the address.

Is it safe to scan a QR code to join Wi-Fi?

Only if you trust whoever put it there. A WIFI: string carries the network name and password and offers to join, and a network controlled by someone else is a position from which unencrypted traffic can be watched or redirected. On a network you did not choose, treat everything as observed.

Why does a code with a logo in it still scan?

Because the symbol carries Reed-Solomon parity, at one of four levels. At the highest, a centred patch covering roughly a sixth of the area still decoded in testing. The logo is sitting in space the error correction can rebuild.

What should I look for in the decoded link?

The registered domain, read from the right: the part immediately before the first single slash. Everything left of it is subdomain and can be made to say anything. Treat a shortener, an xn-- prefix, a raw IP address and an @ sign in the address as reasons to stop.

Related tools