deniz.in

Markets

Weather

Loading weather

· via dev.to (home feed)

Presend adds free QR phishing scan API, writing a PNG decoder from scratch for Cloudflare Workers

Presend's new free API endpoint decodes QR codes from JPEG and PNG uploads and vets the embedded URL against URLhaus, a response to quishing growth the post pegs at 146% year-over-year citing Microsoft.

Presend adds free QR phishing scan API, writing a PNG decoder from scratch for Cloudflare Workers

Presend has added a QR-code phishing scanner to its free API, and getting it working required building a PNG decoder by hand. In a dev.to post, the team behind the tool walks through the new endpoint and the runtime constraints that turned a routine library integration into a from-scratch implementation.

The motivation is the spread of "quishing" — phishing delivered through QR codes. The post cites Microsoft's 2026 report as putting year-over-year growth at 146% in the first quarter, with QR codes now behind roughly 12% of all phishing attacks, up from under 1% in 2021. The vector works because the malicious link sits inside an image, so text-based email filters never see the URL.

What the endpoint does

The new route, POST /api/qr-scan, accepts an uploaded image in JPEG or PNG form, decodes any QR code found in it, and — when the decoded content is a URL — checks that URL against URLhaus in the same request. It is free to use with no signup and no API key, in line with the rest of Presend's API. The point is to give mail pipelines a single call that combines decoding with a reputation check, closing the gap that image-embedded links create.

Why the PNG decoder had to be written

The API runs on Cloudflare Workers, an environment with no Canvas, no Node Buffer and no filesystem, plus a tight limit on script size. QR decoding itself was the easy part: jsQR is pure JavaScript with no dependencies, and jpeg-js handles JPEG once its useTArray option swaps an internal Buffer.alloc call for Uint8Array.

PNG was harder. pngjs, the standard JavaScript library for the format, depends on Node's built-in zlib module, which Workers does not provide. Workers does, however, expose DecompressionStream('deflate') — a web-standard API — and PNG pixel data is deflate-compressed, so decompression could be handed to the runtime. Everything else had to be written: parsing the chunk structure (IHDR for dimensions and color type, one or more IDAT chunks of compressed pixel data, IEND to close the file), reversing the per-scanline filter — a leading byte from 0 to 4 selecting None, Sub, Up, Average or Paeth — and reassembling the pixels. The author validated the decoder against a PNG with known pixel values at its corners and center and got exact matches.

The case that broke it was the common one

Feeding the finished decoder a real QR code produced an error: only 8-bit PNGs were supported, and the image was 1-bit. Most QR code generators, including the Python qrcode library used to build the author's test fixtures, emit 1-bit PNGs by default, since a black-and-white image needs only one bit per pixel. In other words, the path the endpoint was built for was the one it initially could not handle, and shipping as-is would have failed on most real-world QR codes.

The fix meant supporting sub-byte pixel packing: multiple pixels share a single byte, most significant bit first, with each scanline's data padded out to a whole byte. One spec detail mattered along the way — for filter reconstruction, the distance-back "bytes per pixel" value is defined as 1 for any bit depth below 8, regardless of the actual depth.

A note on debugging in the right runtime

jsQR ships as a UMD bundle whose environment detection assigns to self, which crashes under plain Node in an ES-module context. The author's first instinct was to patch the library, but Workers — like browsers — expose self as a real global, and testing against the actual runtime showed it worked unmodified. The broader lesson flagged in the post: a local Node script and an edge runtime are different environments, and this kind of mismatch is likely to keep tripping developers up as more runtimes diverge from the assumptions about Node baked into much of npm.

Why it matters

Quishing is growing quickly precisely because it evades filters that only inspect text, and the growth figures the post attributes to Microsoft describe a meaningful shift in how phishing links are delivered. A free, keyless endpoint that decodes a QR image and vets the URL in one call lowers the barrier for anyone running a mail flow to add image-aware checks. The engineering story also previews a wider trend: as edge runtimes like Workers proliferate, packages coupled to Node internals break, and portable web-standard APIs such as DecompressionStream become the safer foundation. Presend has published the code on GitHub, with the decoder in vendor/png-decoder.js and the endpoint in functions/api/qr-scan.js.

  • #phishing
  • #qr-codes
  • #cloudflare-workers
  • #api
  • #png

Related posts