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),
},
}
}