Skip to content

mono - fix: authenticated ciphers only, bridge setMany() results, serializer __proto__ - #2187

Merged
jaredwray merged 6 commits into
mainfrom
claude/dreamy-ramanujan-5sjbg9
Oct 4, 2026
Merged

jaredwray merged 6 commits into
mainfrom
claude/dreamy-ramanujan-5sjbg9

Conversation

@jaredwray

@jaredwray jaredwray commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

Please check if the PR fulfills these requirements

  • Followed the Contributing and Code of Conduct guidelines.
  • Tests for the changes have been added (for bug fixes/features) with 100% code coverage.

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 encodings

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.
  • 77 accepted algorithms (CBC, CFB, CTR, OFB) are unauthenticated: anyone who can write to the store can change values, or read them by watching for decryption errors (a padding oracle).

It now takes an allowlist: aes-128/192/256-gcm, aes-128/192/256-ccm and chacha20-poly1305 (NodeAlgorithm), and base64, base64url or hex (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-GCM

KeyvEncryptWeb also offered AES-CBC, with the same tampering problem. It now accepts only aes-128/192/256-gcm (WebAlgorithm). The lookup uses a Map, so names like constructor no longer pass the check and fail later. The output format is unchanged, and a test decrypts values written by the previous code.

3. keyv: bridge setMany() reports the writes a store rejects

KeyvBridgeAdapter.setMany() returned true for every entry. With a native setMany it dropped the store's result, and without one it ignored each set() result, so keyv.setMany() reported success for writes a legacy store rejected while keyv.set() returned false. It now maps the store's result back to the entries: one boolean per live entry, one for the batch, or nothing (v5 typed setMany as Promise<void>), which still counts as success. Only an explicit false marks a failed write.

4. keyv: the JSON serializer keeps own __proto__ keys

prepare() copied objects into a plain {}, so an own __proto__ key (as JSON.parse creates from a request body) hit the prototype setter and was dropped. v5's @keyv/serialize kept it. The copy now has no prototype. Reading back is unchanged and safe: JSON.parse defines __proto__ as an own property and never sets a prototype.

Verification

  • Each fix's new tests fail on main and pass here.
  • pnpm test passes in core/keyv (370 tests), encryption/encrypt-node (26) and encryption/encrypt-web (22), with lint clean and every branch of the changed source files covered.
  • With the rebuilt 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 --noEmit on encrypt-node reports no errors. encrypt-web's one CryptoKey type error exists on main too, and both packages build.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wm1wtCGzQJEyYZdekgNVge

…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

codecov Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (790eb3c) to head (d302102).

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread website/site/docs/migration/v5-to-v6.md
Comment thread encryption/encrypt-node/src/index.ts
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-03T23:50:14.569525Z 6b506f2 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

claude added 3 commits October 3, 2026 23:53
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
@jaredwray jaredwray changed the title encrypt-node - fix: accept only authenticated ciphers and text-safe encodings mono - fix: authenticated ciphers only, bridge setMany() results, serializer __proto__ Oct 3, 2026
claude added 2 commits October 3, 2026 23:59
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
@jaredwray
jaredwray merged commit b06ec8b into main Oct 4, 2026
16 checks passed
@jaredwray
jaredwray deleted the claude/dreamy-ramanujan-5sjbg9 branch October 4, 2026 13:32
@jaredwray jaredwray mentioned this pull request Oct 4, 2026
8 of 10 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants