What's wrong
WindowsCredentialStore.TryLoad and WindowsCredentialStore.Save (CredentialCache/Storage/WindowsCredentialStore.cs) copy the plaintext, serialized Credential JSON into managed and unmanaged buffers and then let those buffers go stale without zeroing them — unlike MacOsCredentialStore, which clears its equivalent transient buffer in both TryLoad and Save.
-
TryLoad:
byte[] blob = new byte[cred.CredentialBlobSize];
Marshal.Copy(cred.CredentialBlob, blob, 0, blob.Length);
credential = CredentialSerialization.Deserialize(blob);
return credential is not null;
blob (the raw plaintext bytes of the stored secret) is never cleared. It stays on the managed heap, subject to GC promotion/compaction, until eventually collected — potentially for a long time — and can end up in a crash/memory dump. Compare with MacOsCredentialStore.TryLoad, which calls Array.Clear(blob, 0, blob.Length); right after deserializing.
-
Save:
IntPtr blobPtr = Marshal.AllocHGlobal(blob.Length);
try
{
Marshal.Copy(blob, 0, blobPtr, blob.Length);
// ... CredWrite(...) ...
}
finally
{
Marshal.FreeHGlobal(blobPtr);
Array.Clear(blob, 0, blob.Length);
}
The managed copy (blob) is cleared, but the unmanaged copy at blobPtr — holding the identical plaintext bytes handed to CredWriteW — is freed via Marshal.FreeHGlobal without being zeroed first. Freed unmanaged memory is not scrubbed by the CLR or the OS heap allocator, so the plaintext credential remains in the process's heap until that memory page happens to be reused, and can be recovered by memory-scanning tools or found in a crash dump/hibernation file/page file in the meantime.
Why it matters
This is the platform-native store whose entire purpose is to keep secrets out of ordinary memory/disk exposure. Leaving plaintext copies un-scrubbed defeats part of that goal on Windows specifically, while the macOS implementation in the same codebase already does the right thing — so this is an inconsistency with a known-good pattern already present in the repo, not a speculative hardening request.
Suggested fix
- In
TryLoad, call Array.Clear(blob, 0, blob.Length); after CredentialSerialization.Deserialize(blob) (mirroring MacOsCredentialStore.TryLoad).
- In
Save, zero the unmanaged buffer before freeing it, e.g.:
finally
{
if (blobPtr != IntPtr.Zero)
{
for (int i = 0; i < blob.Length; i++)
{
Marshal.WriteByte(blobPtr, i, 0);
}
Marshal.FreeHGlobal(blobPtr);
}
Array.Clear(blob, 0, blob.Length);
}
Acceptance criteria
- No path in
WindowsCredentialStore.TryLoad/Save frees or abandons a buffer holding plaintext credential bytes without zeroing it first.
- Existing round-trip tests continue to pass.
What's wrong
WindowsCredentialStore.TryLoadandWindowsCredentialStore.Save(CredentialCache/Storage/WindowsCredentialStore.cs) copy the plaintext, serializedCredentialJSON into managed and unmanaged buffers and then let those buffers go stale without zeroing them — unlikeMacOsCredentialStore, which clears its equivalent transient buffer in bothTryLoadandSave.TryLoad:blob(the raw plaintext bytes of the stored secret) is never cleared. It stays on the managed heap, subject to GC promotion/compaction, until eventually collected — potentially for a long time — and can end up in a crash/memory dump. Compare withMacOsCredentialStore.TryLoad, which callsArray.Clear(blob, 0, blob.Length);right after deserializing.Save:The managed copy (
blob) is cleared, but the unmanaged copy atblobPtr— holding the identical plaintext bytes handed toCredWriteW— is freed viaMarshal.FreeHGlobalwithout being zeroed first. Freed unmanaged memory is not scrubbed by the CLR or the OS heap allocator, so the plaintext credential remains in the process's heap until that memory page happens to be reused, and can be recovered by memory-scanning tools or found in a crash dump/hibernation file/page file in the meantime.Why it matters
This is the platform-native store whose entire purpose is to keep secrets out of ordinary memory/disk exposure. Leaving plaintext copies un-scrubbed defeats part of that goal on Windows specifically, while the macOS implementation in the same codebase already does the right thing — so this is an inconsistency with a known-good pattern already present in the repo, not a speculative hardening request.
Suggested fix
TryLoad, callArray.Clear(blob, 0, blob.Length);afterCredentialSerialization.Deserialize(blob)(mirroringMacOsCredentialStore.TryLoad).Save, zero the unmanaged buffer before freeing it, e.g.:Acceptance criteria
WindowsCredentialStore.TryLoad/Savefrees or abandons a buffer holding plaintext credential bytes without zeroing it first.