From d3e891f2f3136d632c395e49135db4818f827956 Mon Sep 17 00:00:00 2001 From: Oskar Eichler Date: Sun, 30 Aug 2026 11:21:11 +0200 Subject: [PATCH] Fix XMLHttpRequest prototype-key headers --- .../Libraries/Network/XMLHttpRequest.js | 15 ++++++----- .../Network/__tests__/XMLHttpRequest-test.js | 25 +++++++++++++++++++ 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/packages/react-native/Libraries/Network/XMLHttpRequest.js b/packages/react-native/Libraries/Network/XMLHttpRequest.js index 3affe5a0e199..0c2fd66c30b5 100644 --- a/packages/react-native/Libraries/Network/XMLHttpRequest.js +++ b/packages/react-native/Libraries/Network/XMLHttpRequest.js @@ -191,11 +191,11 @@ class XMLHttpRequest extends EventTarget { this._cachedResponse = undefined; this._hasError = false; - this._headers = {}; + this._headers = Object.create(null); this._response = ''; this._responseType = ''; this._sent = false; - this._lowerCaseResponseHeaders = {}; + this._lowerCaseResponseHeaders = Object.create(null); this._clearSubscriptions(); this._timedOut = false; @@ -671,13 +671,12 @@ class XMLHttpRequest extends EventTarget { setResponseHeaders(responseHeaders: ?Object): void { this.responseHeaders = responseHeaders || null; const headers = responseHeaders || {}; - this._lowerCaseResponseHeaders = Object.keys(headers).reduce<{ - [string]: any, - }>((lcaseHeaders, headerName) => { + const lowerCaseResponseHeaders: Object = Object.create(null); + for (const headerName of Object.keys(headers)) { // $FlowFixMe[invalid-computed-prop] - lcaseHeaders[headerName.toLowerCase()] = headers[headerName]; - return lcaseHeaders; - }, {}); + lowerCaseResponseHeaders[headerName.toLowerCase()] = headers[headerName]; + } + this._lowerCaseResponseHeaders = lowerCaseResponseHeaders; } setReadyState(newState: number): void { diff --git a/packages/react-native/Libraries/Network/__tests__/XMLHttpRequest-test.js b/packages/react-native/Libraries/Network/__tests__/XMLHttpRequest-test.js index 70fa857e2a8e..adbcda61d397 100644 --- a/packages/react-native/Libraries/Network/__tests__/XMLHttpRequest-test.js +++ b/packages/react-native/Libraries/Network/__tests__/XMLHttpRequest-test.js @@ -295,6 +295,20 @@ describe('XMLHttpRequest', function () { expect(xhr._headers['content-type']).toBe('application/json, text/plain'); }); + it('should support request headers that match Object prototype keys', function () { + xhr.open('GET', 'blabla'); + xhr.setRequestHeader('Constructor', 'first'); + xhr.setRequestHeader('constructor', 'second'); + xhr.setRequestHeader('__proto__', 'value'); + + // $FlowFixMe[prop-missing] + const headers = xhr._headers; + expect(headers.constructor).toBe('first, second'); + expect(Object.getOwnPropertyDescriptor(headers, '__proto__')?.value).toBe( + 'value', + ); + }); + it('should throw when setRequestHeader is called before open', function () { expect(() => { xhr.setRequestHeader('foo', 'bar'); @@ -318,4 +332,15 @@ describe('XMLHttpRequest', function () { 'also-here: Mr. PB\r\newok: lego\r\nfoo-test: 1, 2\r\n__custom: token\r\n', ); }); + + it('should support response headers that match Object prototype keys', function () { + xhr.setResponseHeaders({ + Constructor: 'constructor-value', + ['__proto__']: 'proto-value', + }); + + expect(xhr.getResponseHeader('constructor')).toBe('constructor-value'); + expect(xhr.getResponseHeader('__proto__')).toBe('proto-value'); + expect(xhr.getResponseHeader('toString')).toBe(null); + }); });