diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 783b32e66073..6f7e85274e18 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -10,6 +10,7 @@ _Released 6/18/2024 (PENDING)_ **Bugfixes:** - Fixed an issue where `inlineSourceMaps` was still being used when `sourceMaps` was provided in a users typescript config for typescript version 5. Fixes [#26203](https://github.com/cypress-io/cypress/issues/26203). +- Fixed an issue where receiving HTTP responses with invalid headers raised an error. Now cypress removes the invalid headers and gives a warning in the console with debug mode on. Fixes [#28865](https://github.com/cypress-io/cypress/issues/28865). **Dependency Updates:** diff --git a/packages/errors/__snapshot-html__/PROXY_ENCOUNTERED_INVALID_HEADER_NAME.html b/packages/errors/__snapshot-html__/PROXY_ENCOUNTERED_INVALID_HEADER_NAME.html new file mode 100644 index 000000000000..ae96f6d249d2 --- /dev/null +++ b/packages/errors/__snapshot-html__/PROXY_ENCOUNTERED_INVALID_HEADER_NAME.html @@ -0,0 +1,47 @@ + + + + + + + + + + + +
Warning: While proxying a GET request to http://localhost:8080, an HTTP header did not pass validation, and was removed. This header will not be present in the response received by the application under test.
+
+Invalid header name: 
+
+{
+  "invalidHeaderName": "Value"
+}
+
+Error: fail whale
+
+
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/PROXY_ENCOUNTERED_INVALID_HEADER_VALUE.html b/packages/errors/__snapshot-html__/PROXY_ENCOUNTERED_INVALID_HEADER_VALUE.html new file mode 100644 index 000000000000..5ff625dd5b59 --- /dev/null +++ b/packages/errors/__snapshot-html__/PROXY_ENCOUNTERED_INVALID_HEADER_VALUE.html @@ -0,0 +1,47 @@ + + + + + + + + + + + +
Warning: While proxying a GET request to http://localhost:8080, an HTTP header value did not pass validation, and was removed. This header will not be present in the response received by the application under test.
+
+Invalid header value: 
+
+{
+  "invalidHeaderValue": "Value"
+}
+
+Error: fail whale
+
+
\ No newline at end of file diff --git a/packages/errors/src/errors.ts b/packages/errors/src/errors.ts index 319f1250ddd9..df9b2621be37 100644 --- a/packages/errors/src/errors.ts +++ b/packages/errors/src/errors.ts @@ -1808,6 +1808,26 @@ export const AllCypressErrors = { If you're experiencing problems, downgrade dependencies and restart Cypress. ` }, + + PROXY_ENCOUNTERED_INVALID_HEADER_NAME: (header: any, method: string, url: string, error: Error) => { + return errTemplate` + Warning: While proxying a ${fmt.highlight(method)} request to ${fmt.url(url)}, an HTTP header did not pass validation, and was removed. This header will not be present in the response received by the application under test. + + Invalid header name: ${fmt.code(JSON.stringify(header, undefined, 2))} + + ${fmt.highlightSecondary(error)} + ` + }, + + PROXY_ENCOUNTERED_INVALID_HEADER_VALUE: (header: any, method: string, url: string, error: Error) => { + return errTemplate` + Warning: While proxying a ${fmt.highlight(method)} request to ${fmt.url(url)}, an HTTP header value did not pass validation, and was removed. This header will not be present in the response received by the application under test. + + Invalid header value: ${fmt.code(JSON.stringify(header, undefined, 2))} + + ${fmt.highlightSecondary(error)} + ` + }, } as const // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/packages/errors/test/unit/visualSnapshotErrors_spec.ts b/packages/errors/test/unit/visualSnapshotErrors_spec.ts index e4709fd26b09..0af8e88b6cf6 100644 --- a/packages/errors/test/unit/visualSnapshotErrors_spec.ts +++ b/packages/errors/test/unit/visualSnapshotErrors_spec.ts @@ -1376,5 +1376,21 @@ describe('visual error templates', () => { default: [], } }, + + PROXY_ENCOUNTERED_INVALID_HEADER_NAME: () => { + const err = makeErr() + + return { + default: [{ invalidHeaderName: 'Value' }, 'GET', 'http://localhost:8080', err], + } + }, + + PROXY_ENCOUNTERED_INVALID_HEADER_VALUE: () => { + const err = makeErr() + + return { + default: [{ invalidHeaderValue: 'Value' }, 'GET', 'http://localhost:8080', err], + } + }, }) }) diff --git a/packages/proxy/lib/http/response-middleware.ts b/packages/proxy/lib/http/response-middleware.ts index e88a5273a6e4..7d16182983b1 100644 --- a/packages/proxy/lib/http/response-middleware.ts +++ b/packages/proxy/lib/http/response-middleware.ts @@ -22,6 +22,8 @@ import type { IncomingMessage, IncomingHttpHeaders } from 'http' import { cspHeaderNames, generateCspDirectives, nonceDirectives, parseCspHeaders, problematicCspDirectives, unsupportedCSPDirectives } from './util/csp-header' import { injectIntoServiceWorker } from './util/service-worker-injector' +import { validateHeaderName, validateHeaderValue } from 'http' +import error from '@packages/errors' export interface ResponseMiddlewareProps { /** @@ -306,7 +308,42 @@ const OmitProblematicHeaders: ResponseMiddleware = function () { 'connection', ]) - this.res.set(headers) + this.debug('The headers are %o', headers) + + // Filter for invalid headers + const filteredHeaders = Object.fromEntries( + Object.entries(headers).filter(([key, value]) => { + try { + validateHeaderName(key) + if (Array.isArray(value)) { + value.forEach((v) => validateHeaderValue(key, v)) + } else if (value !== undefined) { + validateHeaderValue(key, value) + } else { + error.warning('PROXY_ENCOUNTERED_INVALID_HEADER_VALUE', { [key]: value }, this.req.method, this.req.originalUrl, new TypeError('Header value is undefined while expecting string')) + + return false + } + + return true + } catch (err) { + if (err.code === 'ERR_INVALID_HTTP_TOKEN') { + error.warning('PROXY_ENCOUNTERED_INVALID_HEADER_NAME', { [key]: value }, this.req.method, this.req.originalUrl, err) + } else if (err.code === 'ERR_INVALID_CHAR') { + error.warning('PROXY_ENCOUNTERED_INVALID_HEADER_VALUE', { [key]: value }, this.req.method, this.req.originalUrl, err) + } else { + // rethrow any other errors + throw err + } + + return false + } + }), + ) + + this.res.set(filteredHeaders) + + this.debug('the new response headers are %o', this.res.getHeaderNames()) span?.setAttributes({ experimentalCspAllowList: this.config.experimentalCspAllowList, diff --git a/packages/proxy/test/unit/http/response-middleware.spec.ts b/packages/proxy/test/unit/http/response-middleware.spec.ts index 859b1c361860..39c91fd49b02 100644 --- a/packages/proxy/test/unit/http/response-middleware.spec.ts +++ b/packages/proxy/test/unit/http/response-middleware.spec.ts @@ -275,6 +275,41 @@ describe('http/response-middleware', function () { }) }) + let badHeaders = { + 'bad-header ': 'value', //(contains trailling space) + 'Content Type': 'value', //(contains a space) + 'User-Agent:': 'value', //(contains a colon) + 'Accept-Encoding;': 'value', //(contains a semicolon) + '@Origin': 'value', //(contains an at symbol) + 'Authorization?': 'value', //(contains a question mark) + 'X-My-Header/Version': 'value', //(contains a slash) + 'Referer[1]': 'value', //(contains square brackets) + 'If-None-Match{1}': 'value', //(contains curly braces) + 'X-Forwarded-For<1>': 'value', //(contains angle brackets) + } + + it('removes invalid headers and leaves valid headers', function () { + prepareContext({ ...badHeaders, 'good-header': 'value' }) + + return testMiddleware([OmitProblematicHeaders], ctx) + .then(() => { + expect(ctx.res.set).to.have.been.calledOnce + expect(ctx.res.set).to.be.calledWith(sinon.match(function (actual) { + // Check if the invalid headers are removed + for (let header in actual) { + if (header in badHeaders) { + throw new Error(`Unexpected header "${header}"`) + } + } + + // Check if the valid header is present + expect(actual['good-header']).to.equal('value') + + return true + })) + }) + }) + const validCspHeaderNames = [ 'content-security-policy', 'Content-Security-Policy', @@ -444,6 +479,7 @@ describe('http/response-middleware', function () { setHeader: sinon.stub(), on: (event, listener) => {}, off: (event, listener) => {}, + getHeaderNames: () => Object.keys(ctx.incomingRes.headers), }, } }