DEV Community

Ikkun
Ikkun

Posted on • Originally published at trustless-security.com

SQLite forensics: why deleting rows doesn't erase secrets (FTS, free pages, VACUUM)

You deleted the row. The secret is gone from the app, the queries return
nothing, and the dashboard is clean. In SQLite — the database behind most
session stores, browser profiles, and agent state files — that delete is a
fiction. The bytes are still in the file.

Three ways deleted data survives

1. Free pages. SQLite doesn't zero out the space a deleted row occupied.
The page is marked free and added to the freelist; the old bytes stay until
they're overwritten by a future write. A file that's been deleted-from is a
forensics goldmine: recover the freelist pages and the "deleted" rows come
back.

2. FTS virtual tables. If the database uses SQLite's full-text search
(FTS5), the FTS index keeps its own copies of the indexed text, maintained
separately from the source tables. Delete the row from the source table and
the FTS index still contains the tokens — searchable. This is the one that
catches people: their app shows the secret is gone, and the FTS index still
has it.

3. WAL and journal files. In WAL mode, recent writes live in the
-wal file; transactions in the -journal file. Both can retain pre-delete
content until checkpointed or cleaned.

"Deleted" in SQLite means "no longer referenced", not "no longer present".

What erasure actually requires

Making a secret physically disappear from a SQLite database takes three
operations, in order:

  1. Replace the value everywhere it lives. Known secret values get
    replaced across all tables; pattern matches (API key formats) get masked.
    Two layers, because you can't enumerate every secret that leaked.

  2. Rebuild the FTS indexes. INSERT INTO t(t) VALUES('rebuild') style
    rebuilds, or drop/recreate the virtual tables — so the index no longer
    contains the old tokens.

  3. Run VACUUM. VACUUM rewrites the entire database file, copying only
    live data into a fresh file — free pages with old bytes are discarded in
    the process. After VACUUM, the file's raw bytes no longer contain the
    secret. (Note: VACUUM doesn't shrink WAL files; those need a checkpoint
    too.)

The design that makes scrubbing safe

Retroactive scrubbing is a destructive operation, so the tooling around it
matters as much as the SQL:

  • Dry-run by default. Scan and report per-table hit counts without writing a byte. You see what would be scrubbed before anything changes.
  • Backup option. Keep a .bak copy before applying changes.
  • Minimum length guard. Don't let pattern rules shred short, low-value strings — only secrets above a minimum length get masked.

Where this bites agent setups

Agent session databases are exactly the SQLite databases where this matters:
they accumulate everything the agent read, including the config values,
environment dumps, and debug output that occasionally contain keys. A
credential strategy that includes "the past is scrubbed" closes the loop
that prevention and DLP can't — the retrospective exposure.

trustless implements this as
trustless dlp scrub-db: two-layer redaction (known values + gitleaks-style
patterns), FTS rebuild, VACUUM, dry-run default, --backup, and --min-len
— with the same pattern rules that power the outbound DLP. The command
reference is in the documentation.

Next time someone tells you a secret was "deleted" from a database, ask them
two questions: did the FTS index get rebuilt, and did anyone run VACUUM?

Top comments (0)