Skip to content

WindowsCredentialStore doesn't clear decrypted credential bytes from memory after use #144

Description

@matt-edmondson

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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions