GetSecretVersion copies decrypted secrets out of locked memory into plain []byte #37
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
From the 1.0 security survey. The largest remaining memory-hygiene hole, because it is on the hot path of the tool's most-used command.
Threat
internal/vault/secrets.go:303-305:The value is decrypted into a
memguard.LockedBuffer, then deliberately copied out into ordinary heap and the locked buffer destroyed. The returned[]byteis unlocked, swappable, never zeroed, and garbage-collected. Every singlesecret gettherefore ends with the full plaintext secret sitting in unprotected memory for the remainder of the process lifetime and beyond.Destroying the locked buffer immediately after copying makes this worse rather than better: it gives the appearance of careful cleanup while the plaintext has just been moved somewhere that cannot be cleaned up at all.
The codebase already knows the correct pattern —
DecryptWithIdentity(internal/secret/crypto.go:96-100) zeroes its scratch slice after use.internal/vault/secrets.gosimply does not follow it.Exposure is the usual set: swap, hibernation, core dumps,
/proc/<pid>/memfor a same-uid process. The distinguishing factor here is reach: this is not a key that protects data, it is the data, and it applies to every secret the user has ever read. Combined with the missingmemguard.Purge()on exit paths (tracked separately), asecret getfollowed by an error exit leaves the plaintext in memory with nothing having wiped it.Definition of done
Vault.GetSecretandVault.GetSecretVersionreturn a*memguard.LockedBufferrather than[]byte, so the plaintext never leaves locked memory inside the library.internal/cliare updated — roughly three to four call sites — and each destroys the buffer on every path, including error paths.stringor[]bytecopy.--version, and that binary secrets containing NUL bytes and non-UTF-8 sequences survive intact — a change of this shape is exactly where astringconversion silently mangles binary data.make checkgreen.TODO.mdupdated in the same commit.Implementation requirements
[]bytesignature and zeroing the slice in the caller. Callers forget, and the whole point is to make the unsafe thing impossible rather than merely discouraged.io.Writer. Avoidfmt.Println,fmt.Fprintf("%s"), and any path that converts tostringfirst, each of which allocates an unprotected copy and reintroduces the bug at the last step.secret getcarefully — it is user-visible and scripts depend on it. Whatever the current behavior is, preserve it exactly, and add a test that pins it if none exists.internal/vault/secrets.gois heavily rewritten there.