Phish-report picked up six new pieces of detection logic this round, merged to main via PR #5. Below is what shipped, and more importantly, what the review process caught before any of it went out the door. The review wasn't a formality: it found a critical bug that would not have been obvious from reading the code casually, plus four more issues ranging from high to medium severity. All of it got fixed and re-verified against the original exploit, not just read over and assumed fine.
What shipped
Sender and Reply-To domain typosquat detection
The tool already had typosquat and brand-impersonation logic for links in a message body. What it didn't have was that same logic pointed at the sending identity itself. checkDomainImpersonation() in lib/authCheck.ts reuses the existing detection against the From and Reply-To domains directly.
This matters because a message like support@paypa1.com with zero suspicious links anywhere in the body used to sail through every link-based check untouched. There was nothing for those checks to look at. Now the sender identity gets the same scrutiny as any URL in the message.
From and Reply-To are checked independently on purpose. A clean, legitimate-looking From address paired with a typosquatted Reply-To is a specific BEC pattern: the visible sender looks fine, but replies get routed to the lookalike domain. Checking only one or the other would miss it.
QR code decoding
New module, lib/qrCheck.ts, decodes QR codes out of embedded images using jsqr, pngjs, and jpeg-js. All pure JS, no shelling out to an external binary, no native image library dependency. Whatever URL comes out of a decoded code gets folded into the same Links table as any other URL in the message and scored the same way.
This closes a gap the tool used to admit to directly in its own output: "QR codes in images are not decoded." That's the kind of caveat that's fine to state honestly while it's true, but better to just fix.
ZIP central-directory listing
lib/zipCheck.ts reads a ZIP's central directory, entry names and sizes only. It never decompresses or executes anything inside the archive. That's enough to catch the classic double-extension trick, something like Invoice.pdf.exe sitting inside the zip, and upgrade what used to be a flat, low-severity "Archive Attachment" note into a critical "Archive Contains a Disguised Executable" finding when it's warranted.
Fixed multi-hop Authentication-Results handling
This one's a bug fix, not a new feature, and it's the one I'd flag as most important in this batch. The old logic took every Authentication-Results header present in a message, joined them into one string, and regex-scanned the whole thing for pass/fail language. The problem: a single forged header, appended anywhere in the message, could inject a benign "passed" result into that combined string and suppress a genuine hard authentication failure sitting in a different header.
Fixed with selectAuthoritativeAuthResults(), which picks the authoritative header by counting Received headers, an order-independent signal, rather than trusting wherever the header happened to land in the message.
Fixed a false confidence-gap message
Small fix, real impact on report quality. Every ordinary, attachment-free email used to get a line saying attachment metadata wasn't present, worded identically to a genuine parsing failure. Since attachment-free mail is the overwhelmingly common case, this meant the tool was constantly flagging normal behavior as a limitation. Fixed so that not having an attachment is no longer reported as though something failed to parse.
Per-category score bars
The UI now shows native <progress> elements per scoring category, authentication, content, links, attachments, and so on, indicating how close each category sits to its own cap. An analyst can now tell at a glance whether a score is being driven by one maxed-out category or genuine breadth across several, instead of having to infer that from the signal list.
What the review caught
Implementation was followed by an independent code review and a security audit run in parallel against the diff, before any of it was committed. This wasn't rubber-stamping. Here's what came back.
Critical: a PNG decompression bomb bypassed the QR decoder's own size guard. The guard checked declared pixel dimensions before decoding, which reads as sufficient on its face. It wasn't. The decoding library, pngjs, takes a completely unbounded decompression path for interlaced PNGs specifically, ignoring the declared dimensions entirely on that code path. A 1x1-pixel, roughly 400KB interlaced PNG was measured inflating to around 400MB on decode, over 1000:1. Scaled up within the tool's existing upload size limits, a single request could have forced multi-gigabyte memory allocation. Fixed by rejecting interlaced PNGs outright, a QR code has no legitimate reason to ever be interlaced, plus an independent hard ceiling on decompressed size as defense in depth, so the fix doesn't rely on the interlacing check alone.
High: the forged-header auth bug had a second layer. There was already a compensating check meant to catch exactly this kind of forgery, cross-referencing the mail server identity claimed in Authentication-Results against the actual delivery chain. It turned out that check independently selected a different header than the one the primary auth logic trusted, so it could never actually see the forged header in the first place. Two checks, each individually reasonable, disagreeing about which header was ground truth. Fixed by making both checks resolve to the same authoritative header.
High: the QR decoder was fully synchronous. A handful of images in a single request could block the entire Node process for seconds, within the tool's own existing rate limit, meaning no additional abuse was even required to trigger it. Fixed with a lower pixel cap on decoded images and a hard cumulative time budget across a single scan.
Medium, three findings:
- An extension-checking bypass using invisible Unicode: NUL, zero-width characters, right-to-left override, could hide a dangerous file extension from detection entirely.
- ZIP-format evasion via ZIP64 archives, a forged entry count, or simply renaming the archive's file extension.
- ZIP entry filenames weren't sanitized before being rendered in the report, meaning a maliciously named entry inside the archive could visually spoof its own extension on screen.
Every one of these was re-verified after the fix by reproducing the original exploit against the patched code and confirming it no longer works. Not eyeballed, actually run.
Known limitations going in
Worth being direct about what's still open, since none of this closes every gap:
- QR decoding still briefly blocks the event loop. The fix bounds it to roughly 750ms worst case, it doesn't eliminate the block entirely. This is a single-process app with no worker pool for CPU-bound work, so that tradeoff is inherent to the current architecture, not an oversight in this round.
- A ZIP whose central-directory offset is itself corrupted or forged isn't recoverable by brute-force scanning the archive. That kind of scan risks false matches inside compressed data, so the tool doesn't attempt it.
- A fully self-consistent, entirely fabricated delivery chain, not one forged trailing header but a complete fake
ReceivedplusAuthentication-Resultschain built from scratch, is still invisible to this tool. It makes no DNS or network calls and has no cryptographic way to verify that any givenReceivedline is genuine. That's a fundamentally different problem than the header-ordering bug fixed this round.
Verification
Five sample .eml files were added under sample-emails/ so the new detections can be exercised by hand against the live tool: a clean baseline, a sender typosquat case, a QR-code-in-image case, a multi-hop forwarded email with a genuine mid-chain DMARC failure, and a ZIP containing a disguised executable.
Test suite grew from 186 to 196 passing tests across this round, covering both the new detections and the fixes that came out of review.
Summary
| Area | Status |
|---|---|
| Sender/Reply-To typosquat detection | Shipped |
| QR code decoding | Shipped, decompression bomb fixed |
| ZIP disguised-executable detection | Shipped, evasion paths closed |
| Multi-hop Authentication-Results bug | Fixed |
| Auth chain cross-reference (authserv-id) | Fixed to agree with primary check |
| False confidence-gap messaging | Fixed |
| Per-category score bars | Shipped |
| QR decoder event-loop blocking | Bounded, not eliminated |
| Fabricated delivery chains | Still out of scope |
The pattern worth keeping from this round: build the feature, then throw an adversarial review at the diff before it ships, not after. The decompression bomb in particular wasn't the kind of thing that shows up in normal testing. It only shows up when someone goes looking for the one code path that ignores your own guard.