Skip to content

fix(sync): don't reject recoverable data as tampering (#9256) - #9259

Merged
johannesjo merged 2 commits into
masterfrom
feat/issue-9256-7a4187
Jul 23, 2026
Merged

fix(sync): don't reject recoverable data as tampering (#9256)#9259
johannesjo merged 2 commits into
masterfrom
feat/issue-9256-7a4187

Conversation

@johannesjo

Copy link
Copy Markdown
Collaborator

Fixes #9256.

The problem

A SuperSync (E2EE) user could not recover their data after a fresh install. After entering the correct encryption password, sync aborted with an OperationIntegrityError"possible sync-server tampering" (GHSA-8pxh-mgc7-gp3g). It's a false positive: the server data is fine.

Console signature from the report:

firstErrorPath: $input.issueProvider.entities["…"].allowFetchFallback  (expected: boolean)
[assertDecryptedFullStateOpIntegrity] … rejecting (possible sync-server tampering)

Root cause (two layers)

  1. Stale data (rule 11 violation). Allows to use self-hosted Jira instance without web extension #7628 (v18.10.0) added allowFetchFallback and altPublicLinkHost to JiraCfg as required fields with no ? and no backfill. A Jira provider configured before that persists without them.
  2. The wall (v18.15.0, fix(sync): reject forged encrypted full-state operations #8984). assertDecryptedFullStateOpIntegrity runs strict typia validation on the decrypted SYNC_IMPORT in the download path and throws on any error — before the apply path's own Checkpoint‑D repair (dataRepair → autoFixTypiaErrors) could heal the drift. So the security gate was stricter than the app's own loader, turning benign version‑drift into a hard rejection and blocking recovery.

The fix

  • Type the two fields optional (rule 11). All read sites already treat a missing value as the default, and DEFAULT_JIRA_CFG still sets both.
  • Make the integrity check heal‑tolerant. When strict validation fails but every error is field‑level drift within a present root, mirror the apply path's autoFixTypiaErrors on a throwaway structuredClone and accept if it then validates. A missing top‑level root — the opType‑promotion attack shape, where a single‑entity op is relabelled SYNC_IMPORT — still rejects without healing.
  • Guard the one hole a naive heal leaves. typia descends into a present‑but‑mis‑typed root (e.g. globalConfig: []) and reports only nested errors, which autoFixTypiaErrors' globalConfig catch‑all would rebuild from defaults. _hasWrongRootContainerKind rejects any root whose container kind (array vs object) disagrees with the model, so a mis‑typed section can never heal through. Fails closed if the heal probe throws.

The security boundary is those two structural filters — not what autoFixTypiaErrors happens to be able to rebuild. A promoted single‑entity payload is missing whole top‑level sections and auto‑fix never fabricates them, so the GHSA‑8pxh‑mgc7‑gp3g promotion defense is preserved.

Testing

verify-decrypted-op-integrity.spec.ts gains 7 unit tests against the real frozen-state-v18.15 snapshot: accepts a valid snapshot; accepts one missing the #7628 fields (regression for #9256); heals a still‑required drifted field; rejects a promoted single‑entity payload, a snapshot missing a whole section, and a mis‑typed root (globalConfig: []); ignores non‑full‑state ops. The heal‑path and container‑kind tests were sabotage‑verified (each fails on the wrong implementation).

This change was hardened via a multi‑agent review — the globalConfig: [] container‑kind gap was caught by an adversarial pass and closed here.

A SuperSync (E2EE) user could not recover their data after a fresh
install: sync aborted with an OperationIntegrityError ("possible
sync-server tampering", GHSA-8pxh-mgc7-gp3g). It was a false positive.

Root cause, two layers:
- #7628 (v18.10.0) added `allowFetchFallback` and `altPublicLinkHost`
  to JiraCfg as REQUIRED fields with no `?` and no backfill (rule 11).
  A Jira provider configured earlier persists without them.
- The v18.15.0 full-state integrity check ran strict typia validation
  on the decrypted SYNC_IMPORT in the download path and threw on ANY
  error — before the apply path's own Checkpoint D repair could heal
  the drift. So the security gate was stricter than the app's own
  loader and turned benign version-drift into a hard rejection.

Fix:
- Type the two fields optional (rule 11); all read sites already treat
  a missing value as the default and DEFAULT_JIRA_CFG still sets both.
- Make the integrity check heal-tolerant: when strict validation fails
  but every error is field-level drift within a present root, mirror
  the apply path's autoFixTypiaErrors on a throwaway clone and accept
  if it then validates. A missing top-level root (the opType-promotion
  attack shape) still rejects without healing.
- Guard the one hole a naive heal leaves: typia descends into a
  present-but-mis-typed root (e.g. `globalConfig: []`) and reports only
  nested errors, which autoFixTypiaErrors' globalConfig catch-all would
  rebuild from defaults. `_hasWrongRootContainerKind` rejects any root
  whose container kind disagrees with the model, so a mis-typed section
  can never heal through. Fail closed if the heal probe throws.

The security boundary is those two structural filters, not what
autoFixTypiaErrors happens to be able to rebuild.
@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Status URL
Deployed https://8f2b6023.super-productivity-preview.pages.dev

Branch: feat/issue-9256-7a4187
Commit: 2974d65


Deployed with Cloudflare Pages

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🚨 Unable to Recover Data from SuperSync

1 participant