fix(ext/sw): free org handle on openOrg error path (no org-key leak past lock)
Security review I1: openOrg's org_unwrap_key inserts the org master key into the WASM session at step 7, but on the non-cached manifest-error path the handle was never returned / registered via session.setOrg — so session.clearAll() (lock + inactivity timeout) could not see it, and the org key would outlive a failed open (GC-backstopped, non-deterministic). Triggers on a corrupt manifest.enc or a first-switch network error with no cache. Free the handle before rethrowing. Adds a test asserting the error path frees the handle. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
This commit is contained in:
@@ -370,4 +370,28 @@ describe('org-vault', () => {
|
|||||||
// No cachedManifest → must rethrow
|
// No cachedManifest → must rethrow
|
||||||
await expect(orgVault.openOrg(ORG_CFG, w)).rejects.toThrow('Failed to fetch');
|
await expect(orgVault.openOrg(ORG_CFG, w)).rejects.toThrow('Failed to fetch');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Test 8 (I1): a non-cached error path must FREE the unwrapped handle before
|
||||||
|
// rethrowing — otherwise the org master key (inserted into the WASM session by
|
||||||
|
// org_unwrap_key) is never registered via session.setOrg, so clearAll() can't
|
||||||
|
// see it and the key would outlive a subsequent lock/timeout.
|
||||||
|
it('openOrg: a non-network manifest error frees the unwrapped handle before rethrowing', async () => {
|
||||||
|
const host = makeOrgHost({
|
||||||
|
'members.json': enc(JSON.stringify(MEMBERS_JSON)),
|
||||||
|
[`keys/${MEMBER_ID}.enc`]: WRAPPED_KEY_BYTES,
|
||||||
|
'manifest.enc': MANIFEST_ENC_BYTES,
|
||||||
|
});
|
||||||
|
vi.mocked(gitHostMod.createGitHost).mockReturnValue(host);
|
||||||
|
const w = makeOrgWasm({
|
||||||
|
// Decrypt throws a non-network error (e.g. corrupt manifest.enc) → else branch.
|
||||||
|
org_manifest_decrypt_filtered: vi.fn(() => { throw new Error('corrupt manifest'); }),
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(orgVault.openOrg(ORG_CFG, w)).rejects.toThrow('corrupt manifest');
|
||||||
|
|
||||||
|
// The handle was created (org key live in WASM) ...
|
||||||
|
expect(w.org_unwrap_key).toHaveBeenCalledWith(WRAPPED_KEY_BYTES);
|
||||||
|
// ... and must be freed on the error path so the org key does not persist.
|
||||||
|
expect(w._handle.free).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -109,6 +109,12 @@ export async function openOrg(
|
|||||||
manifest = cachedManifest;
|
manifest = cachedManifest;
|
||||||
offline = true;
|
offline = true;
|
||||||
} else {
|
} else {
|
||||||
|
// The org master key is live in the WASM session (org_unwrap_key inserted
|
||||||
|
// it at step 7), but this handle was never returned / registered via
|
||||||
|
// session.setOrg — so session.clearAll() (lock + inactivity timeout) cannot
|
||||||
|
// see it, and the key would outlive a failed open. Free it before
|
||||||
|
// propagating so the org key never persists past this error path.
|
||||||
|
try { handle.free(); } catch { /* best-effort: still propagate the original error */ }
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user