Skip to content

Windows Save leaves the serialized plaintext credential unscrubbed when it exceeds the 2560-byte blob limit #165

Description

@matt-edmondson

What's wrong

WindowsCredentialStore.Save (CredentialCache/Storage/WindowsCredentialStore.cs, ~lines 75–108) serializes the credential into byte[] blob and then checks its size before entering the try { … } finally { CredentialSerialization.Zero(blob); } block:

byte[] blob = CredentialSerialization.Serialize(credential);
if (blob.Length > NativeMethods.CRED_MAX_CREDENTIAL_BLOB_SIZE)
{
    throw new CredentialStoreException(...);   // blob is never zeroed on this path
}

try
{
    ...
}
finally
{
    CredentialSerialization.Zero(blob);
}

When the size check throws, the full plaintext JSON (username/password or token) stays on the managed heap until the GC reclaims it, and it is never zeroed.

Why it matters

The recent scrubbing work (e.g. 97a49ab, "scrub Linux Secret Service plaintext like Windows and macOS") commits the library to zeroing every managed plaintext copy on every path, including when an exception is thrown. The macOS Save and the Linux path both honour this. The Windows over-limit path is the only one that doesn't.

Concrete scenario: someone stores a CredentialWithToken holding a long JWT or PAT whose serialized form is over 2560 bytes. Save throws CredentialStoreException as it should, but the serialized secret is left in memory unscrubbed. It is then exposed to memory dumps, crash dumps and swap, which is exactly what the scrubbing is meant to prevent.

Suggested fix

Move the size check inside the try, so the finally covers it:

byte[] blob = CredentialSerialization.Serialize(credential);
try
{
    if (blob.Length > NativeMethods.CRED_MAX_CREDENTIAL_BLOB_SIZE)
    {
        throw new CredentialStoreException(...);
    }
    ...
}
finally
{
    CredentialSerialization.Zero(blob);
}

Acceptance criteria

  • blob is zeroed on every exit path of WindowsCredentialStore.Save, including the over-limit throw.
  • If the shared-layer tests can observe it, add a test that saves an oversize credential and asserts the buffer was scrubbed. At minimum, add a test asserting the throw still happens.

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

    readyFully specified; implement as written

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions