fix: pass inflateRaw override to decryptResponse for large cy.prompt payloads (#33619)

* fix: pass inflateRaw override to decryptResponse for large cy.prompt payloads

The jose library caps decompressed JWE payloads at ~250KB by default. Larger
cy.prompt /plan responses (which carry cached selectors and chains) inflated
past that limit and produced DecryptionError: decryption operation failed.
Match the 5MB ceiling the services-side @packages/encryption decrypt already
configures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Update CHANGELOG for version 15.14.1

Added changelog entry for version 15.14.1 with bugfix details.

* Update CHANGELOG.md

* Apply suggestion from @ryanthemanuel

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ryan Manuel
2026-04-17 16:18:13 -05:00
committed by GitHub
parent f4fdff61f5
commit b3202f0ae9
3 changed files with 60 additions and 3 deletions
+9
View File
@@ -1,4 +1,13 @@
<!-- See ../guides/writing-the-cypress-changelog.md for details on writing the changelog. -->
## 15.14.1
_Released 04/30/2026 (PENDING)_
**Bugfixes:**
- Increased the limit for decrypted payloads to support large `cy.prompt` requests and responses. Fixed in [#33619](https://github.com/cypress-io/cypress/pull/33619).
## 15.14.0
_Released Apr 16, 2026_
+13 -3
View File
@@ -1,12 +1,22 @@
import crypto, { BinaryLike } from 'crypto'
import { TextEncoder, promisify } from 'util'
import { generalDecrypt, GeneralJWE } from 'jose'
import { DecryptOptions, generalDecrypt, GeneralJWE } from 'jose'
import base64Url from 'base64url'
import type { CypressRequestOptions } from './api'
import { deflateRaw as deflateRawCb } from 'zlib'
import { deflateRaw as deflateRawCb, inflateRaw as inflateRawCb } from 'zlib'
import fs from 'fs'
const deflateRaw = promisify(deflateRawCb)
const inflateRaw = promisify(inflateRawCb)
// The `jose` library caps decompressed JWE payloads at ~250KB by default, which
// is too small for larger cy.prompt `/plan` responses (these carry cached
// selectors/chains that can inflate past that threshold). Override with a 5MB
// ceiling so decryption matches the server-side inflate limit configured in
// `@packages/encryption` in cypress-services.
const DECRYPT_OPTIONS: DecryptOptions = {
inflateRaw: (data) => inflateRaw(data, { maxOutputLength: 1024 * 1024 * 5 }),
}
const CY_TEST = `LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFyZ0pGb2FuMFROTUxWSUNvZWF2WQpJVWtqNWJaM2QxTlVJdVU4WjM1b2hzcUFGWHY1eGhRRzd5MWdkeTR1SVZOSFU0a1RmUERBZEk1MnRWcGE5UG1yCm5OSDhsZE5kMjRwemVFNm1CeE91MlVDQ1d3VEY5eS9BUGZqNjVkczJSSTMwR09oZm95Q1pyQndxRU1zdWJ1MTUKVVNqbFBUcEFoWlFZN2Y2bHA4TTZMYU55SUhLMzYyMDgvZFp6aWs4Q1NLWmwya0E0TUN4eWxhUElWNVVWZG0rRQpkdjJhdm1Hcy9vQzVUV1VRNzlVSmJyMDllem1oZWExcS81VnpvajRvODlKSkNnelFhcllvL1QrNVlreWJ0Z2hkCjN3NnNPSjBCQ2NrUUV5MGpXVWJWUnhSa084VGJsckxXbC9Rd0Ryd1EvRERQaEZaSGhIbCtCL3JRTldsYTFXdEoKclFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0t`
const CY_STAGING = `LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFxZE1OazZYVkFhV3VlT0lXZ3V2aQpDTlhPRGVtMHRINmo0NnFTWUhJZFcyU1N5NHU0ZFpGd1VHQldlZjBEbDVmeGhZa1BFczBxUDJHUnlUZnY5YjNXCk9xWEJFQmFyNXMyYzJSMGd1RzdqNGtidTlZZklRQWpWejZndUtQMTIzd3VKSjFmcEU5T3pXWlZmUi9pQWl4b1gKTi82aEFhSHNMT1RlNXROdTVESzNOQnUxa3VJTTExcDZScEo5bGgvbWVFK3JObzRZVWUvZ2Jzam1mZmJiODRGeQpqWk1sQW9YSnYxU3lKK2phdTNMa3JkdzMybWYrczMyd2NLUnNpbmp1STgrZndDT2lEb2xnZW9NdEhta2tXVS8xCnJvUnVEcGd6d2FZemxLbUhRODNWQTlOTUhvNmMwVU40MzlBMnVtRFQ4ek94SjJjQUY0U0RiWTV3RnBnd3cvUVgKd1FJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0t`
@@ -111,7 +121,7 @@ export async function encryptRequest (params: Pick<CypressRequestOptions, 'body'
* decrypts the response payload, which is assumed to be JSON
*/
export async function decryptResponse (jwe: GeneralJWE, encryptionKey: crypto.KeyObject): Promise<any> {
const result = await generalDecrypt(jwe, encryptionKey)
const result = await generalDecrypt(jwe, encryptionKey, DECRYPT_OPTIONS)
const plaintext = Buffer.from(result.plaintext).toString('utf8')
return JSON.parse(plaintext)
@@ -66,6 +66,44 @@ describe('encryption', () => {
expect(roundtripResponse).to.eql(RESPONSE_BODY)
})
// Regression: jose's default inflateRaw caps decompressed payloads at ~250KB,
// which caused large cy.prompt /plan responses to fail with
// "DecryptionError: decryption operation failed". decryptResponse must pass
// DecryptOptions with a higher maxOutputLength to match the server-side limit.
it('decrypts response payloads larger than the jose default inflate limit', async () => {
const { jwe, secretKey } = await encryption.encryptRequest({
encrypt: true,
body: TEST_BODY,
}, { publicKey })
const unwrappedKey = crypto.privateDecrypt(privateKey, Buffer.from(jwe.recipients[0].encrypted_key, 'base64'))
const unwrappedSecretKey = crypto.createSecretKey(unwrappedKey)
// Build a payload whose uncompressed JSON is larger than jose's ~250KB
// default but still well within our 5MB ceiling. Use random bytes per entry
// so the DEFLATE layer can't trivially compress it away.
const LARGE_RESPONSE = {
items: Array.from({ length: 800 }, (_, i) => ({
id: i,
xpath: `//body/div[${i}]`,
innerText: crypto.randomBytes(256).toString('hex'),
})),
}
expect(JSON.stringify(LARGE_RESPONSE).length).to.be.greaterThan(400 * 1024)
const enc = new jose.GeneralEncrypt(
Buffer.from(JSON.stringify(LARGE_RESPONSE)),
)
enc.setProtectedHeader({ alg: 'A256GCMKW', enc: 'A256GCM', zip: 'DEF' }).addRecipient(unwrappedSecretKey)
const jweResponse = await enc.encrypt()
const roundtripResponse = await encryption.decryptResponse(jweResponse, secretKey)
expect(roundtripResponse).to.eql(LARGE_RESPONSE)
})
describe('verifySignatureFromFile', () => {
it('verifies a valid signature from a file', async () => {
const filePath = path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'encryption', 'index.js')