@@ -112,6 +112,12 @@ export function preventXHR(source, propsToMatch, customResponseText) {
112112 const nativeGetResponseHeader = window . XMLHttpRequest . prototype . getResponseHeader ;
113113 const nativeGetAllResponseHeaders = window . XMLHttpRequest . prototype . getAllResponseHeaders ;
114114
115+ // Store matched XHR requests and their data in private structures
116+ // to prevent bypass via thisArg property manipulation
117+ // https://github.com/AdguardTeam/Scriptlets/issues/386
118+ const matchedXhrRequests = new Map ( ) ;
119+ const xhrRequestHeaders = new Map ( ) ;
120+
115121 let xhrData ;
116122 let modifiedResponse = '' ;
117123 let modifiedResponseText = '' ;
@@ -126,19 +132,21 @@ export function preventXHR(source, propsToMatch, customResponseText) {
126132 logMessage ( source , `xhr( ${ objectToString ( xhrData ) } )` , true ) ;
127133 hit ( source ) ;
128134 } else if ( matchRequestProps ( source , propsToMatch , xhrData ) ) {
129- thisArg . shouldBePrevented = true ;
130- // Add xhrData to thisArg to keep original values in case of multiple requests
135+ // Store xhrData in map to keep original values in case of multiple requests
131136 // https://github.com/AdguardTeam/Scriptlets/issues/347
132- thisArg . xhrData = xhrData ;
137+ matchedXhrRequests . set ( thisArg , xhrData ) ;
133138 }
134139
135140 // Trap setRequestHeader of target xhr object to mimic request headers later;
136141 // needed for getResponseHeader() and getAllResponseHeaders() methods
137- if ( thisArg . shouldBePrevented ) {
138- thisArg . collectedHeaders = [ ] ;
142+ if ( matchedXhrRequests . has ( thisArg ) && ! xhrRequestHeaders . has ( thisArg ) ) {
143+ xhrRequestHeaders . set ( thisArg , [ ] ) ;
139144 const setRequestHeaderWrapper = ( target , thisArg , args ) => {
140145 // Collect headers
141- thisArg . collectedHeaders . push ( args ) ;
146+ const headers = xhrRequestHeaders . get ( thisArg ) ;
147+ if ( headers ) {
148+ headers . push ( args ) ;
149+ }
142150 return Reflect . apply ( target , thisArg , args ) ;
143151 } ;
144152 const setRequestHeaderHandler = {
@@ -152,10 +160,12 @@ export function preventXHR(source, propsToMatch, customResponseText) {
152160 } ;
153161
154162 const sendWrapper = ( target , thisArg , args ) => {
155- if ( ! thisArg . shouldBePrevented ) {
163+ if ( ! matchedXhrRequests . has ( thisArg ) ) {
156164 return Reflect . apply ( target , thisArg , args ) ;
157165 }
158166
167+ const storedXhrData = matchedXhrRequests . get ( thisArg ) ;
168+
159169 if ( thisArg . responseType === 'blob' ) {
160170 modifiedResponse = new Blob ( ) ;
161171 }
@@ -198,7 +208,7 @@ export function preventXHR(source, propsToMatch, customResponseText) {
198208 Object . defineProperties ( thisArg , {
199209 readyState : { value : 4 , writable : false } ,
200210 statusText : { value : 'OK' , writable : false } ,
201- responseURL : { value : responseURL || thisArg . xhrData . url , writable : false } ,
211+ responseURL : { value : responseURL || storedXhrData . url , writable : false } ,
202212 responseXML : { value : responseXML , writable : false } ,
203213 status : { value : 200 , writable : false } ,
204214 response : { value : modifiedResponse , writable : false } ,
@@ -237,15 +247,18 @@ export function preventXHR(source, propsToMatch, customResponseText) {
237247 thisArg . dispatchEvent ( loadEndEvent ) ;
238248 } , 1 ) ;
239249
240- nativeOpen . apply ( forgedRequest , [ thisArg . xhrData . method , thisArg . xhrData . url ] ) ;
250+ nativeOpen . apply ( forgedRequest , [ storedXhrData . method , storedXhrData . url ] ) ;
241251
242252 // Mimic request headers before sending
243253 // setRequestHeader can only be called on open request objects
244- thisArg . collectedHeaders . forEach ( ( header ) => {
254+ const collectedHeaders = xhrRequestHeaders . get ( thisArg ) || [ ] ;
255+ collectedHeaders . forEach ( ( header ) => {
245256 const name = header [ 0 ] ;
246257 const value = header [ 1 ] ;
247258 forgedRequest . setRequestHeader ( name , value ) ;
248259 } ) ;
260+ // Note: We do NOT delete from xhrRequestHeaders here because
261+ // getResponseHeader() and getAllResponseHeaders() need access to the headers later
249262
250263 return undefined ;
251264 } ;
@@ -260,16 +273,17 @@ export function preventXHR(source, propsToMatch, customResponseText) {
260273 * @returns {string|null } Header value or null if header is not set.
261274 */
262275 const getHeaderWrapper = ( target , thisArg , args ) => {
263- if ( ! thisArg . shouldBePrevented ) {
276+ const collectedHeaders = xhrRequestHeaders . get ( thisArg ) ;
277+ if ( ! collectedHeaders ) {
264278 return nativeGetResponseHeader . apply ( thisArg , args ) ;
265279 }
266- if ( ! thisArg . collectedHeaders . length ) {
280+ if ( ! collectedHeaders . length ) {
267281 return null ;
268282 }
269283 // The search for the header name is case-insensitive
270284 // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/getResponseHeader
271285 const searchHeaderName = args [ 0 ] . toLowerCase ( ) ;
272- const matchedHeader = thisArg . collectedHeaders . find ( ( header ) => {
286+ const matchedHeader = collectedHeaders . find ( ( header ) => {
273287 const headerName = header [ 0 ] . toLowerCase ( ) ;
274288 return headerName === searchHeaderName ;
275289 } ) ;
@@ -287,13 +301,14 @@ export function preventXHR(source, propsToMatch, customResponseText) {
287301 * @returns {string } All headers as a string. For no headers an empty string is returned.
288302 */
289303 const getAllHeadersWrapper = ( target , thisArg ) => {
290- if ( ! thisArg . shouldBePrevented ) {
304+ const collectedHeaders = xhrRequestHeaders . get ( thisArg ) ;
305+ if ( ! collectedHeaders ) {
291306 return nativeGetAllResponseHeaders . call ( thisArg ) ;
292307 }
293- if ( ! thisArg . collectedHeaders . length ) {
308+ if ( ! collectedHeaders . length ) {
294309 return '' ;
295310 }
296- const allHeadersStr = thisArg . collectedHeaders
311+ const allHeadersStr = collectedHeaders
297312 . map ( ( header ) => {
298313 /**
299314 * TODO: array destructuring may be used here
0 commit comments