What's wrong
WindowsCredentialStore builds the Credential Manager target name as $"{_servicePrefix}:{persona}" (CredentialCache/Storage/WindowsCredentialStore.cs:32). EnumerateKeys (lines ~130-157) filters with CredEnumerate("{prefix}:*") and treats everything after "{prefix}:" as the persona.
Nothing stops the service name from containing :. CredentialStoreFactory.CreateDefault(serviceName) accepts any non-empty name and encourages per-app names. Two services where one name is a :-prefix of the other therefore share a namespace.
On Linux and macOS, service and account are separate attributes, so this only affects Windows.
Failure scenario
App A uses service "MyApp" and App B (or a dev build) uses "MyApp:Dev". B saves a persona abcd…, stored as target MyApp:Dev:abcd….
- A's
EnumerateKeys() matches MyApp:* and returns a persona "Dev:abcd…".
- A's
TryLoad of that persona rebuilds MyApp:Dev:abcd… and returns B's credential.
- A's
Remove of that persona deletes B's credential.
So a "remove all my personas" loop in A wipes B's stored credentials.
This was found by tracing the code. It was not run, because no Windows host was available. The path is deterministic string handling.
Suggested fix
Any one of these:
- Reject
: and * in the Windows service prefix in the constructor or factory.
- In
EnumerateKeys, skip any remainder that is not a valid persona GUID or that still contains :.
- Escape the service component when building the target name, or record the service in the credential's
UserName/attributes and filter on that.
Acceptance criteria
- A store for
"MyApp" never enumerates, loads or removes a credential written by a store for "MyApp:Dev".
- A test covers the prefix-collision case. It can be Windows-only, or a pure test of the target-name and enumeration-filter logic if that is factored out.
What's wrong
WindowsCredentialStorebuilds the Credential Manager target name as$"{_servicePrefix}:{persona}"(CredentialCache/Storage/WindowsCredentialStore.cs:32).EnumerateKeys(lines ~130-157) filters withCredEnumerate("{prefix}:*")and treats everything after"{prefix}:"as the persona.Nothing stops the service name from containing
:.CredentialStoreFactory.CreateDefault(serviceName)accepts any non-empty name and encourages per-app names. Two services where one name is a:-prefix of the other therefore share a namespace.On Linux and macOS, service and account are separate attributes, so this only affects Windows.
Failure scenario
App A uses service
"MyApp"and App B (or a dev build) uses"MyApp:Dev". B saves a personaabcd…, stored as targetMyApp:Dev:abcd….EnumerateKeys()matchesMyApp:*and returns a persona"Dev:abcd…".TryLoadof that persona rebuildsMyApp:Dev:abcd…and returns B's credential.Removeof that persona deletes B's credential.So a "remove all my personas" loop in A wipes B's stored credentials.
This was found by tracing the code. It was not run, because no Windows host was available. The path is deterministic string handling.
Suggested fix
Any one of these:
:and*in the Windows service prefix in the constructor or factory.EnumerateKeys, skip any remainder that is not a valid persona GUID or that still contains:.UserName/attributes and filter on that.Acceptance criteria
"MyApp"never enumerates, loads or removes a credential written by a store for"MyApp:Dev".