mono - fix: authenticated ciphers only, bridge setMany() results, serializer __proto__ - #2187
Conversation
…ncodings KeyvEncryptNode accepted any algorithm Node's getCipherInfo() knows and any BufferEncoding. 32 of the algorithm names it accepted failed on the first encrypt() (ECB and other ciphers without an IV, aes-256-xts, chacha20 treated as AEAD, OCB without a tag length, and more), utf8 and ascii output never decrypted, and utf16le failed whenever the ciphertext length was odd. The 77 unauthenticated modes it accepted (CBC, CFB, CTR, OFB) let anyone who can write to the store change values, or read them through decryption errors. The adapter now takes an explicit allowlist, like @keyv/encrypt-web: aes-128/192/256-gcm, aes-128/192/256-ccm and chacha20-poly1305, typed as NodeAlgorithm, and base64, base64url or hex, typed as NodeEncoding. Anything else throws when the adapter is created. Every cipher now sets a 16-byte tag length explicitly, so a truncated tag is rejected. The output format is unchanged, and a test decrypts values written by the previous code. The READMEs, the encryption overview and the docs index no longer list CBC for encrypt-node or describe the two packages as cross-compatible, which was never a goal; encrypt-web's class comment drops the same claim. The migration guide's encryption table lists the new set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wm1wtCGzQJEyYZdekgNVge
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2187 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 56 56
Lines 5798 5781 -17
Branches 997 990 -7
=========================================
- Hits 5798 5781 -17 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6b506f22c3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
KeyvEncryptWeb also offered AES-CBC (128/192/256), which encrypts without an integrity check: anyone who can write to the store can change a value, or read it by watching for decryption errors. It now accepts only aes-128/192/256-gcm, typed as WebAlgorithm, and throws when the adapter is created with anything else. The lookup now uses a Map, so names such as `constructor` or `toString` no longer pass the check and fail later. The output format is unchanged, and a test decrypts values written by the previous code. The README and the encryption overview list only AES-GCM. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wm1wtCGzQJEyYZdekgNVge
KeyvBridgeAdapter.setMany() returned `true` for every entry whatever the wrapped store reported. With a native setMany it dropped the store's result, and without one it ignored what each set() returned, so keyv.setMany() reported success for writes a legacy store rejected, while keyv.set() on the same store returned `false`. The fallback now returns each set() result. The native path maps the store's result back to the entries, past the already-expired ones it deletes: one boolean per live entry, one boolean for the batch, or nothing (v5 typed setMany as Promise<void>), which still counts as success. Only an explicit `false` marks a failed write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wm1wtCGzQJEyYZdekgNVge
prepare() copied each object into a plain `{}` with `result[key] =`, so
an own `__proto__` key, which JSON.parse creates from input such as a
request body, hit the prototype setter instead of being copied and was
silently dropped. v5's @keyv/serialize kept it.
The copy now has no prototype, so `__proto__` is stored like any other
key. Reading it back is unchanged and safe: JSON.parse defines it as an
own property and never sets a prototype.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wm1wtCGzQJEyYZdekgNVge
Codex review on #2187 pointed out that values a 6.0.0 pre-release encrypted with a cipher or encoding this release rejects can't be decrypted after the upgrade, and that the keyv-migrate skill didn't mention the narrowed options. A decrypt-only path for those ciphers would keep the padding oracle the change removes, so the encrypt-node and encrypt-web READMEs instead explain how to re-encrypt such values on the pre-release before upgrading, or let a cache refill. The skill's adapters reference lists the accepted algorithms and encodings and tells agents to ask the user which of those two to do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wm1wtCGzQJEyYZdekgNVge
The encryption adapters haven't been released yet, so no stored values can need re-encrypting. This reverts 454b6ab, which added "Upgrading from a 6.0.0 pre-release" sections to the encrypt-node and encrypt-web READMEs and a matching note to the keyv-migrate skill. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wm1wtCGzQJEyYZdekgNVge
Please check if the PR fulfills these requirements
What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)
Bug fixes and security hardening, with docs. These are the remaining pre-GA fixes, after jaredwray/keyv#2185 and jaredwray/keyv#2186, one per commit. (A fifth commit added notes on migrating pre-release ciphertext and a sixth reverts it: the encryption adapters haven't been released, so there is nothing to migrate.)
1.
encrypt-node: accept only authenticated ciphers and text-safe encodingsKeyvEncryptNodeaccepted any algorithm Node'sgetCipherInfo()knows and anyBufferEncoding:encrypt(): ECB and other ciphers without an IV,aes-256-xts,chacha20treated as AEAD, OCB without a tag length, and more.utf8andasciioutput never decrypted, andutf16lefailed whenever the ciphertext length was odd.It now takes an allowlist:
aes-128/192/256-gcm,aes-128/192/256-ccmandchacha20-poly1305(NodeAlgorithm), andbase64,base64urlorhex(NodeEncoding). Anything else throws when the adapter is created. Every cipher sets a 16-byte tag length, so a truncated tag is rejected. The output format is unchanged, and a test decrypts values written by the previous code. Compatibility between encrypt-node and encrypt-web was never a goal, so the cross-compatibility sections are gone from both READMEs, the encryption overview and encrypt-web's class comment.2.
encrypt-web: accept only AES-GCMKeyvEncryptWebalso offered AES-CBC, with the same tampering problem. It now accepts onlyaes-128/192/256-gcm(WebAlgorithm). The lookup uses aMap, so names likeconstructorno longer pass the check and fail later. The output format is unchanged, and a test decrypts values written by the previous code.3.
keyv: bridgesetMany()reports the writes a store rejectsKeyvBridgeAdapter.setMany()returnedtruefor every entry. With a nativesetManyit dropped the store's result, and without one it ignored eachset()result, sokeyv.setMany()reported success for writes a legacy store rejected whilekeyv.set()returnedfalse. It now maps the store's result back to the entries: one boolean per live entry, one for the batch, or nothing (v5 typedsetManyasPromise<void>), which still counts as success. Only an explicitfalsemarks a failed write.4.
keyv: the JSON serializer keeps own__proto__keysprepare()copied objects into a plain{}, so an own__proto__key (asJSON.parsecreates from a request body) hit the prototype setter and was dropped. v5's@keyv/serializekept it. The copy now has no prototype. Reading back is unchanged and safe:JSON.parsedefines__proto__as an own property and never sets a prototype.Verification
mainand pass here.pnpm testpasses incore/keyv(370 tests),encryption/encrypt-node(26) andencryption/encrypt-web(22), with lint clean and every branch of the changed source files covered.keyv, these also pass: bigmap, test-suite, the three compression adapters, both serializers, sqlite, redis (against local Redis), and the website's docs and skill validation.tsc --noEmiton encrypt-node reports no errors. encrypt-web's oneCryptoKeytype error exists onmaintoo, and both packages build.🤖 Generated with Claude Code
https://claude.ai/code/session_01Wm1wtCGzQJEyYZdekgNVge