---
title: "We Attacked Our Own Gmail Extension, Four Times"
description: "The browser extension that checks attachments in Gmail passed its tests and was still wrong. Four rounds of hostile testing found a false SEALED, script injection, signature replay, and a forged evidence certificate."
pubDate: 2026-10-12
author: "Anaiya"
series: "Moonglade Lab Dispatches"
readTime: "6 min read"
heroImage: "/images/staging/2026-10-15-batch1/hero-3-extension-adversarial.jpg"
image: "/images/staging/2026-10-15-batch1/hero-3-extension-adversarial.jpg"
tags:
  - provenance
  - browser-extension
  - security
  - audit
  - engineering
audio: "/audio/2026-10-12-we-attacked-our-own-gmail-extension.140fe418fe82.mp3"
---

![We Attacked Our Own Gmail Extension, Four Times](/images/staging/2026-10-15-batch1/hero-3-extension-adversarial.jpg)

In an internal research build, our browser extension attaches a provenance badge to email attachments inside Gmail. We have held this extension out of our public release while we shift toward a Workspace-side Gmail and Drive integration. Even so, the extension gave us a sharp testbed for a baseline engineering rule: a verifier must never report `SEALED` when it has not earned that verdict.

Over four consecutive test rounds, we tried to force the extension to lie. It did not take much persuasion.

Our workflow separated implementation from testing. One agent wrote each remediation; an adversarial QA agent wrote attack payloads against the fix and executed the test suite, with Zen reviewing the diffs and verdicts. These were local agent-driven audits in our St. Andrew lab, not an external security certification.

![Security engineering test bench still-life: four progressive testing stations across a workshop table, each exhibiting a physical failure mechanism](/images/staging/2026-10-15-batch1/figure-3-adversarial-stations.jpg)

## Round 1: a seal with no signature

The verifier's OpenPGP branch set `signatureValid` to `true` whenever two conditions held: the named signer existed in the local key store, and the SHA-256 hash in the sidecar matched the attachment bytes. It never verified the OpenPGP signature payload itself.

Any sidecar that named a known signer and carried the matching file digest therefore rendered as `SEALED`, even if the signature block contained random bytes.

We wrote the failing tests first. Two test cases passed forged OpenPGP blobs, expected `OPENPGP_UNVERIFIED`, and received `SEALED` before the fix. The remediation introduces a distinct amber status: `OpenPGP signature present but not verified by this verifier`. The green `SEALED` state now requires a verified Ed25519 signature.

That change carries an operational trade-off worth stating directly. Attachments that Zen signed with OpenPGP no longer display green in the browser extension; they display amber until full OpenPGP verification is wired into the browser runtime (capability that already runs in our Obsidian gate and ProvNat store). An honest amber is better engineering than a false green.

## Round 2: script injection from a filename

The extension's audit modal originally assembled its HTML by interpolating strings into a template literal: the attachment filename, signer label, key fingerprint, scope, timestamp fields, and the raw JSON sidecar inside a `<pre>` element. In Gmail, the attachment filename comes straight from the sender's email header, whether or not a sidecar is present.

A crafted filename or sidecar field could therefore inject markup into the extension's content script context. Even `JSON.stringify` inside a `<pre>` tag does not prevent injection because it leaves angle brackets unescaped.

Rather than writing regex sanitizers, we removed HTML string interpolation altogether. Every node in the badge and audit modal is now constructed through `document.createElement` and populated via `textContent`, with zero `innerHTML` assignments remaining in the source. The adversarial test suite instrumented all DOM insertion paths, fed ten injection payload classes across every sidecar field and filename, and verified zero raw HTML writes. We also set the modal's fallback state to `Not Verified`, ensuring an unrecognised status code cannot default to a sealed badge.

## Round 3: a real signature, replayed

Round 3 uncovered the subtler flaw in the set.

The content tamper check compared the attachment's computed SHA-256 digest against an *unsigned* top-level sidecar field, `sha256_at_last_write`. Meanwhile, the Ed25519 signature check verified the signature over a separate field inside the attestation object, `attestation.sha256`. Nothing required those two fields to match. An attacker could take a valid signature from File A, attach it to modified File B, update the unsigned `sha256_at_last_write` field to match File B's digest, and pass both checks.

The rule that closes the gap is strict: an attestation is evaluated only if its signed `sha256` field is a valid 64-character hex digest that exactly equals the digest the extension just computed from the raw attachment bytes. The signed payload is reconstructed directly from that computed file digest rather than sidecar metadata. The verifier also iterates through all matching attestations so an invalid entry at index zero cannot mask a valid signature listed after it.

The QA suite tested mismatched digests, uppercase and whitespace-padded hex strings, null and array injection, and post-signing scope edits. Every case failed closed.

## Round 4: forging the evidence certificate

The extension allows users to export a plain-text verification certificate. Because sidecar fields were written into that text file without newline escaping, a newline character embedded inside a signer name or scope string could inject synthetic lines into the exported certificate, including a fake `Verdict: AUTHENTIC` line.

Although the real verdict remained at the bottom of the file, an exported audit record should never permit line spoofing. Every interpolated field now runs through a strict single-line escape pass. During testing, QA caught a flaw in the first escape implementation where a literal `\n` sequence and an actual newline character produced identical output, making the encoding non-invertible. The revised encoder escapes backslashes first (`\` to `\\`) before escaping control characters, and QA verified it by round-tripping 5,000 randomized adversarial strings through an independent decoder.

Round 4 also tightened three edge behaviors:

- A valid signature bound to a previous filename stays `SEALED` (since the file bytes are intact) but surfaces a prominent filename-mismatch warning.
- A malformed JSON sidecar now renders an explicit `UNVERIFIED` badge instead of failing silently with no indicator.
- Null or duplicate entries in the attestations array are skipped cleanly without throwing unhandled exceptions.

| Round | Defect Identified | Passing Tests After Fix |
|---|---|---|
| Baseline | Initial test suite | 15 |
| Round 1 (OpenPGP) | `SEALED` verdict without signature verification | 20 (50 with adversarial suite) |
| Round 2 (DOM XSS) | HTML/script injection via filename and sidecar fields | 80 |
| Round 3 (Replay) | Valid signature replayed across modified file bytes | 200 |
| Round 4 (Certificate) | Newline injection in exported text certificates | 232 |

*Test counts reflect the repository QA log across identical workspace copies.*

## Enforcing the key-store trust flag

On 8 October we closed one additional gap identified during a local Codex review: the verifier checked that a signer key existed in the key store before returning `SEALED`, but did not check whether that key entry had `trusted: true`. The QA log had noted this as informational, and we updated the verifier in local commit `751847e` so keys marked untrusted cannot produce a `SEALED` verdict. Recompiling the TypeScript source and test suite on 14 October confirmed all 9 test suites and 233 tests passing.

## Scope and limitations

- The earlier competition submission snapshot predates these four hardening rounds and has not been retrofitted with these patches.
- The DOM injection test suite runs against a simulated DOM harness rather than a live headless Chrome instance.
- Plain-text certificates cannot prevent Unicode homoglyph spoofing outside ASCII control ranges.
- All patches and adversarial tests were authored by studio agents and reviewed by Zen. They represent internal engineering verification, not an independent security audit.

*Evidence for the claims above: commits `af46518`, `e396fd5`, `85628a4`, `b294327`, and local commit `751847e`, alongside `QA.md` in the provenance monorepo.*
