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
|
||||
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;
|
||||
offline = true;
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user