Skip to content

TryGet on Windows and macOS frees the OS-returned plaintext buffer without zeroing it, so every load leaves a copy of the secret in freed heap #173

Description

@matt-edmondson

What's wrong

On a cache miss, CredentialCache.TryGet calls Store.TryLoad. On two platforms, the native buffer holding the decrypted credential JSON is freed without being zeroed. Only the managed copy is scrubbed.

  • Windows, CredentialCache/Storage/WindowsCredentialStore.cs:50-65: CredRead returns a CREDENTIAL whose CredentialBlob (CredentialBlobSize bytes) is the plaintext. The code copies it into blob, and DeserializeAndScrub(blob) scrubs the managed array. The finally then calls NativeMethods.CredFree(handle) on the native block untouched.
  • macOS, CredentialCache/Storage/MacOsCredentialStore.cs:51-61: passwordPtr (length bytes) is copied and scrubbed the same way, then released with SecKeychainItemFreeContent(IntPtr.Zero, passwordPtr), which does not zero the buffer (as macOS Save and Remove decrypt the existing secret into process memory without using it, and free it unscrubbed #166 notes).
  • Linux is fine: secret_password_free wipes its own copy.

#166 covers the unnecessary decrypt in macOS Save/Remove, and its acceptance criteria deliberately leave TryGet's handling unchanged. This issue is about that TryGet path itself, on both Windows and macOS.

Why it matters

Each load leaves a full plaintext copy of the serialized credential (password or token JSON) in freed process-heap memory until the allocator reuses it. A crash dump or memory scan can recover it. That contradicts the README's scrubbing guarantee and the acceptance criterion of the closed #144: "No path … frees … a buffer holding plaintext … without zeroing it first".

Suggested fix

  • Add a helper such as NativeSecretBuffer.ZeroExternal(IntPtr ptr, int length) that zeroes a caller-visible native buffer, for example with Marshal.Copy from a zero array, in the same way NativeSecretBuffer.Zero handles its own buffers.
  • Windows: call it on cred.CredentialBlob / CredentialBlobSize before CredFree.
  • macOS: call it on passwordPtr / length before SecKeychainItemFreeContent.

Whether CredFree scrubs internally is undocumented. Zeroing a buffer the caller can already read costs almost nothing, so doing it anyway is the safe choice.

Acceptance criteria

  • Neither the Windows nor the macOS TryLoad frees a native plaintext buffer without zeroing it first, including when deserialization throws. The zeroing belongs in the finally, ahead of the free.
  • The helper has a unit test that runs on every OS.

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

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions