← All advisories

Advisory WM-2026-10-001

Residue verdicts and licence checks in WatchMemory 0.2.0

Three issues in WatchMemory 0.2.0 could make a residue check report the wrong verdict, or let a licence key from another store unlock Pro. None of them sends data off your machine or exposes your app's data to anyone. Update to 0.3.0 (0.2.1 at minimum) and re-run any residue check that passed with 0.2.0.

Published
October 2026
Affected
WatchMemory Mac app, CLI and the documented CI recipe, versions 0.2.0 and earlier
Fixed in
0.2.1 (all three issues mitigated) and 0.3.0 (issue 2 fixed for app storage and disk)
Status
Fixed. No CVE has been requested.

1. The documented CI canary recipe always failed

What happened

The CI guide, the README and the GitHub Actions example passed the canary to the app as a launch argument (-WMCanary WMCANARY_…). Launch arguments stay in the process's memory for its whole lifetime, so the canary was always found after logout.

Impact

False FAIL on every run, whatever the app did on logout. Teams following the recipe could never get a passing gate, which invites disabling the check.

What changed (Fixed in 0.2.1)

  • The app now generates its own canary when it is launched with -WMCanaryMode 1 (or SIMCTL_CHILD_WM_CANARY_MODE=1): call WatchMemory.canaryIfRequested() from WatchMemoryDebugAgent. canaryFromLaunchArguments() is deprecated.
  • Scanning a process whose launch arguments or environment contain WMCANARY_ stops with exit code 2 and explains why, instead of reporting a survivor.
  • The recipe sets SIMCTL_CHILD_MallocScribble=1 and SIMCTL_CHILD_MallocNanoZone=0, so freed native memory is overwritten and not reported as surviving.
  • The CI guide, README and examples were corrected.

2. Opaque refresh tokens in app storage could produce a false PASS

What happened

Tokens without a recognisable format (for example an opaque rt_… refresh token, unlike a JWT) were missed in several storage formats: UserDefaults plists, SQLite key/value rows, redux-persist data in AsyncStorage and MMKV files. When a generic key=value rule did find one, it was rated Medium, below the default fail-on threshold (High).

Impact

A refresh token that survived logout on disk could end in PASS. This affects the correctness of the verdict only; WatchMemory itself did not expose the token.

What changed (Fixed in 0.2.1 (severity), 0.3.0 (detection))

  • 0.2.1: findings stored under a refresh, access, ID, session or auth token key are rated High, so they fail the gate at the default threshold. SQLite write-ahead logs (-wal) are read too.
  • 0.3.0: key-aware extraction from plists, SQLite, JSON (including AsyncStorage and redux-persist), XML preferences, MMKV and DataStore names the exact store and key, for example SDK:redux-persist › persist:root › auth › refreshToken (redux-persist in AsyncStorage).
  • 0.3.0: residue capture --track-from BEFORE.json looks again, by value, for the secrets seen before logout in the storage and files captured after it, even when no pattern matches (as written; re-encoded copies such as base64 are not followed). Only keyed hashes are stored, never the values.
  • Limit: an opaque value that survives only in memory is not tracked yet (planned for 0.4).

3. Licence validation accepted keys from other stores

What happened

In builds where the Lemon Squeezy store and product IDs were not configured, licence validation could accept a licence key issued by a different Lemon Squeezy store.

Impact

Pro features could be unlocked without a WatchMemory purchase. Scan data and results were not affected.

What changed (Fixed in 0.2.1)

  • Release builds carry the WatchMemory store and product IDs and fail closed if they are missing.
  • Cached validations from other stores are deleted on update, with no offline grace period.
  • Keys bought through watchmemory.app keep working. If yours stops working after the update, write to support@watchmemory.app.

What you should do

  1. Update the Mac app to 0.3.0 from the download page or the in-app update banner.

  2. In CI, stop piping the unversioned tarball into tar. Download the versioned CLI, check its SHA-256 against the value you pinned, then unpack it.

    V=0.3.0
    curl -fsSLO https://watchmemory.app/downloads/watchmemory-cli-$V.tar.gz
    # pin the SHA-256 from the release notes in your pipeline, then:
    echo "$WM_CLI_SHA256  watchmemory-cli-$V.tar.gz" | shasum -a 256 -c -
    tar xzf watchmemory-cli-$V.tar.gz
  3. Replace -WMCanary WMCANARY_… launch arguments with the canary-mode recipe, and let the app generate the canary.

    SIMCTL_CHILD_WM_CANARY_MODE=1 SIMCTL_CHILD_MallocScribble=1 SIMCTL_CHILD_MallocNanoZone=0 \
      xcrun simctl launch booted com.acme.app
    
    // in the app (WatchMemoryDebugAgent, DEBUG builds only)
    let token = WatchMemory.canaryIfRequested("access_token") ?? realToken
  4. Re-run every residue check or CI gate that passed with 0.2.0, especially if your app keeps refresh tokens in UserDefaults, AsyncStorage, MMKV or SQLite. Capture both sides again with 0.3.0: comparing a 0.2.x capture with a 0.3.0 capture is refused.

    watchmemory residue capture --process MyApp --bundle-id com.acme.app --out before.json
    # log out
    watchmemory residue capture --process MyApp --bundle-id com.acme.app \
      --track-from before.json --out after.json
    watchmemory residue compare before.json after.json --html residue.html
  5. Existing baselines are still read. Re-record them with 0.3.0 to switch to per-secret matching.

Timeline

  • 2026-10-15: issues found in an internal security and quality review of 0.2.0.
  • 0.2.1: mitigations for all three issues.
  • 0.3.0: key-aware storage extraction and before/after value tracking (issue 2).

Credit: Found by WatchMemory's internal review.

Questions: write to security@watchmemory.app. /en/security