DEV Community

Cover image for Your screenshot tool is the leak — doing redaction entirely client-side
glensum
glensum

Posted on

Your screenshot tool is the leak — doing redaction entirely client-side

The most common way people redact a screenshot is still this:

  1. Take the screenshot.
  2. Search "blur image online".
  3. Upload it to the first result.
  4. Blur the part that matters, download, paste into Slack.

Step 3 is the problem. You just handed an unredacted screenshot — session tokens, customer emails, API keys, production data — to a website you found ninety seconds ago. The blur happens after the upload, and the "delete" button on that page deletes your copy, not theirs.

It's a small, boring leak, and it happens thousands of times a day, because the tool that solves the aesthetic problem (make this text unreadable) creates a security problem (it made a copy first).

The rule I'd propose

Redaction must happen on the machine the screenshot was taken on, before the image goes anywhere. If the pixels reaching any network request are already covered, the rest of the pipeline stops mattering — you can paste it into a chat, a ticket, a bug report, and the sensitive part isn't in the file.

That's the whole design constraint. Everything below falls out of taking it seriously.

Anatomy of a leak, in the browser

A screenshot in a browser is a compositor output. The extension API is chrome.tabs.captureVisibleTab, which returns a data URL of the visible viewport:

const dataUrl = await chrome.tabs.captureVisibleTab({ format: 'png' });
Enter fullscreen mode Exit fullscreen mode

At this point you hold a faithful copy of everything on screen, including the parts nobody wants in a public issue: the token in the address bar, the Authorization header value in some debug panel, the customer list scrolled halfway down.

The naive version of a redaction feature is a UI on top of that image. The important version redacts before you decide to keep the file — i.e. the extension should never write the raw capture anywhere durable.

What "covered" should actually mean

Two choices worth being deliberate about:

Blur vs solid fill. Blur looks more polite, but for text it's a lossy encoding, not a one-way function. Low-radius blurs of high-contrast text (a monospace token on a dark terminal, for instance) have been reconstructed with deconvolution, and text heights are highly constrained, which shrinks the search space. For secrets, use an opaque fill. Blur is for faces and plates, where the content is continuous, not discrete.

Redraw vs overlay. Overlaying a rectangle in an HTML layer is easy and completely useless — the underlying pixels are still in the export. You need to composite (draw the image, then draw the fill on top) and export the re-encoded result:

const off = new OffscreenCanvas(w, h);
const ctx = off.getContext('2d');
ctx.drawImage(imgBitmap, 0, 0);                 // the capture
ctx.fillStyle = '#111';
ctx.fillRect(x, y, w0, h0);                     // irreversible at this point
const blob = await off.convertToBlob({ type: 'image/png' });
Enter fullscreen mode Exit fullscreen mode

Everything downstream operates on that blob. If a cover is wrong, the user redraws; there is no "uncover" and no original in the payload.

The part that actually reduces user error: pre-detection

Manual redaction fails the same way manual everything fails — people miss one spot, usually the boring one. So the useful feature isn't a nicer brush; it's candidate detection before the user looks:

  • DOM-matched regions. Elements with type="password", autocomplete="cc-number", inputmode="numeric", or a name/id matching /token|secret|api[-_]?key|otp|ssn/i. The element's bounding box maps directly to image coordinates via getBoundingClientRect() plus scroll offsets.
  • Text-pattern regions. Run a small regex set over the visible text nodes and highlight matches: JWT shape (eyJ[A-Za-z0-9_-]{10,}\.), sk-/ghp_/AKIA prefixes, IBAN-ish and card-number-ish digit runs, private-key headers.
  • Structural guesses. Terminal/console panes, .env-looking blocks, admin tables — worth flagging, not auto-covering.

The output should be a proposed set of covers the user can delete in one click. Auto-covering without review is its own failure mode: over-covering quietly destroys the screenshot's purpose, and the user then posts a useless image or, worse, turns the feature off.

Where detection honestly fails

I'd rather list the limits than imply a guarantee:

  • Canvas / WebGL / video. If the secret is rendered into a canvas or a <video> frame, there is no DOM node and no text node. Regex detection sees nothing. Only a manual cover catches these.
  • Images of text. A screenshot of a screenshot, a scanned invoice, a chart with labelled series. No OCR, no detection.
  • Anything virtualised. Table cells recycled by a virtual list can be measured before or after they exist in the DOM, depending on scroll timing. Off-by-one-row covers are a real bug class.
  • Shadow DOM. Closed shadow roots are invisible to the traversal; you get a bounding box with no children to inspect.

So the honest product claim is "detection reduces misses; the manual cover is still the guarantee." Any tool claiming otherwise is describing a best case.

What this means for the architecture

Client-side redaction has one more consequence, and it's the reason I'd pick this shape again: there's nothing to operate. No upload endpoint, no object storage, no retention policy to write, no "we delete after 24h" claim to make and be audited on. The security property is structural rather than promised.

For a small tool that's the difference between a feature you can ship and a small compliance project. The trade is that you can't do the heavy stuff on a server — no models, no OCR on the secret-detection step, no history sync. In exchange, the failure mode of your infrastructure is "nothing happens," which is the failure mode I want.

I built this into a small extension — Veilshot. Pricing, stated plainly: the free tier does the manual covers (no account, no network requests at all — that's checked in CI on every build, not just promised in a policy); Pro is a one-off $19 for pattern detection, keyword rules and per-site rules, no subscription. It's a paid product and I'm not going to pretend otherwise. But the pattern is the more useful takeaway, and it generalises well past screenshots: if the data is sensitive, transform it before it moves, not after. Blur tools on the web got the order wrong, and that's the entire bug.

Top comments (0)