Skip to content

Commit 1625d40

Browse files
feat: [#1553] Adds setting disableSameOriginPolicy, to make it possible to bypass the same-origin policy (CORS) (#1554)
* feat: [1553] Add disableCrossOriginPolicy setting to disable cors in the browser * chore: [#1553] Adds unit tests and changes name on setting for disabling same-origin policy * chore: [#1553] Adds unit tests and changes name on setting for disabling same-origin policy --------- Co-authored-by: David Ortner <[email protected]>
1 parent a78cd8f commit 1625d40

9 files changed

+194
-24
lines changed

packages/happy-dom/src/browser/BrowserSettingsFactory.ts

+4
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ export default class BrowserSettingsFactory {
2929
...DefaultBrowserSettings.timer,
3030
...settings?.timer
3131
},
32+
fetch: {
33+
...DefaultBrowserSettings.fetch,
34+
...settings?.fetch
35+
},
3236
device: {
3337
...DefaultBrowserSettings.device,
3438
...settings?.device

packages/happy-dom/src/browser/DefaultBrowserSettings.ts

+3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ export default <IBrowserSettings>{
1818
maxIntervalIterations: -1,
1919
preventTimerLoops: false
2020
},
21+
fetch: {
22+
disableSameOriginPolicy: false
23+
},
2124
navigation: {
2225
disableMainFrameNavigation: false,
2326
disableChildFrameNavigation: false,

packages/happy-dom/src/browser/types/IBrowserSettings.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,28 @@ export default interface IBrowserSettings {
2020
/** Handle disabled resource loading as success */
2121
handleDisabledFileLoadingAsSuccess: boolean;
2222

23-
/** Settings for timers */
23+
/**
24+
* Settings for timers
25+
*/
2426
timer: {
2527
maxTimeout: number;
2628
maxIntervalTime: number;
2729
maxIntervalIterations: number;
2830
preventTimerLoops: boolean;
2931
};
3032

33+
/**
34+
* Settings for fetch
35+
*/
36+
fetch: {
37+
/**
38+
* Disables same-origin policy (CORS)
39+
*
40+
* @see https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy
41+
*/
42+
disableSameOriginPolicy: boolean;
43+
};
44+
3145
/**
3246
* Disables error capturing.
3347
*

packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts

+12
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ export default interface IOptionalBrowserSettings {
2424
maxIntervalIterations?: number;
2525
};
2626

27+
/**
28+
* Settings for fetch
29+
*/
30+
fetch?: {
31+
/**
32+
* Disables same-origin policy (CORS)
33+
*
34+
* @see https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy
35+
*/
36+
disableSameOriginPolicy?: boolean;
37+
};
38+
2739
/**
2840
* Disables error capturing.
2941
*

packages/happy-dom/src/fetch/Fetch.ts

+16-9
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export default class Fetch {
5353
private request: Request;
5454
private redirectCount = 0;
5555
private disableCache: boolean;
56-
private disableCrossOriginPolicy: boolean;
56+
private disableSameOriginPolicy: boolean;
5757
#browserFrame: IBrowserFrame;
5858
#window: BrowserWindow;
5959
#unfilteredHeaders: Headers | null = null;
@@ -69,7 +69,7 @@ export default class Fetch {
6969
* @param [options.redirectCount] Redirect count.
7070
* @param [options.contentType] Content Type.
7171
* @param [options.disableCache] Disables the use of cached responses. It will still store the response in the cache.
72-
* @param [options.disableCrossOriginPolicy] Disables the Cross-Origin policy.
72+
* @param [options.disableSameOriginPolicy] Disables the Same-Origin policy.
7373
* @param [options.unfilteredHeaders] Unfiltered headers - necessary for preflight requests.
7474
*/
7575
constructor(options: {
@@ -80,7 +80,7 @@ export default class Fetch {
8080
redirectCount?: number;
8181
contentType?: string;
8282
disableCache?: boolean;
83-
disableCrossOriginPolicy?: boolean;
83+
disableSameOriginPolicy?: boolean;
8484
unfilteredHeaders?: Headers;
8585
}) {
8686
this.#browserFrame = options.browserFrame;
@@ -95,7 +95,10 @@ export default class Fetch {
9595
}
9696
this.redirectCount = options.redirectCount ?? 0;
9797
this.disableCache = options.disableCache ?? false;
98-
this.disableCrossOriginPolicy = options.disableCrossOriginPolicy ?? false;
98+
this.disableSameOriginPolicy =
99+
options.disableSameOriginPolicy ??
100+
this.#browserFrame.page.context.browser.settings.fetch.disableSameOriginPolicy ??
101+
false;
99102
}
100103

101104
/**
@@ -128,7 +131,11 @@ export default class Fetch {
128131
this.#window.location.protocol === 'https:'
129132
) {
130133
throw new this.#window.DOMException(
131-
`Mixed Content: The page at '${this.#window.location.href}' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint '${this.request.url}'. This request has been blocked; the content must be served over HTTPS.`,
134+
`Mixed Content: The page at '${
135+
this.#window.location.href
136+
}' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint '${
137+
this.request.url
138+
}'. This request has been blocked; the content must be served over HTTPS.`,
132139
DOMExceptionNameEnum.securityError
133140
);
134141
}
@@ -141,7 +148,7 @@ export default class Fetch {
141148
}
142149
}
143150

144-
if (!this.disableCrossOriginPolicy) {
151+
if (!this.disableSameOriginPolicy) {
145152
const compliesWithCrossOriginPolicy = await this.compliesWithCrossOriginPolicy();
146153

147154
if (!compliesWithCrossOriginPolicy) {
@@ -192,7 +199,7 @@ export default class Fetch {
192199
url: this.request.url,
193200
init: { headers, method: cachedResponse.request.method },
194201
disableCache: true,
195-
disableCrossOriginPolicy: true
202+
disableSameOriginPolicy: true
196203
});
197204

198205
if (cachedResponse.etag || !cachedResponse.staleWhileRevalidate) {
@@ -251,7 +258,7 @@ export default class Fetch {
251258
*/
252259
private async compliesWithCrossOriginPolicy(): Promise<boolean> {
253260
if (
254-
this.disableCrossOriginPolicy ||
261+
this.disableSameOriginPolicy ||
255262
!FetchCORSUtility.isCORS(this.#window.location.href, this.request[PropertySymbol.url])
256263
) {
257264
return true;
@@ -303,7 +310,7 @@ export default class Fetch {
303310
url: this.request.url,
304311
init: { method: 'OPTIONS' },
305312
disableCache: true,
306-
disableCrossOriginPolicy: true,
313+
disableSameOriginPolicy: true,
307314
unfilteredHeaders: corsHeaders
308315
});
309316

packages/happy-dom/src/fetch/ResourceFetch.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export default class ResourceFetch {
3434
browserFrame: this.#browserFrame,
3535
window: this.window,
3636
url,
37-
disableCrossOriginPolicy: true
37+
disableSameOriginPolicy: true
3838
});
3939
const response = await fetch.send();
4040

@@ -60,7 +60,7 @@ export default class ResourceFetch {
6060
browserFrame: this.#browserFrame,
6161
window: this.window,
6262
url,
63-
disableCrossOriginPolicy: true
63+
disableSameOriginPolicy: true
6464
});
6565

6666
const response = fetch.send();

packages/happy-dom/src/fetch/SyncFetch.ts

+16-9
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export default class SyncFetch {
3838
private request: Request;
3939
private redirectCount = 0;
4040
private disableCache: boolean;
41-
private disableCrossOriginPolicy: boolean;
41+
private disableSameOriginPolicy: boolean;
4242
#browserFrame: IBrowserFrame;
4343
#window: BrowserWindow;
4444
#unfilteredHeaders: Headers | null = null;
@@ -54,7 +54,7 @@ export default class SyncFetch {
5454
* @param [options.redirectCount] Redirect count.
5555
* @param [options.contentType] Content Type.
5656
* @param [options.disableCache] Disables the use of cached responses. It will still store the response in the cache.
57-
* @param [options.disableCrossOriginPolicy] Disables the Cross-Origin policy.
57+
* @param [options.disableSameOriginPolicy] Disables the Same-Origin policy.
5858
* @param [options.unfilteredHeaders] Unfiltered headers - necessary for preflight requests.
5959
*/
6060
constructor(options: {
@@ -65,7 +65,7 @@ export default class SyncFetch {
6565
redirectCount?: number;
6666
contentType?: string;
6767
disableCache?: boolean;
68-
disableCrossOriginPolicy?: boolean;
68+
disableSameOriginPolicy?: boolean;
6969
unfilteredHeaders?: Headers;
7070
}) {
7171
this.#browserFrame = options.browserFrame;
@@ -80,7 +80,10 @@ export default class SyncFetch {
8080
}
8181
this.redirectCount = options.redirectCount ?? 0;
8282
this.disableCache = options.disableCache ?? false;
83-
this.disableCrossOriginPolicy = options.disableCrossOriginPolicy ?? false;
83+
this.disableSameOriginPolicy =
84+
options.disableSameOriginPolicy ??
85+
this.#browserFrame.page.context.browser.settings.fetch.disableSameOriginPolicy ??
86+
false;
8487
}
8588

8689
/**
@@ -118,7 +121,11 @@ export default class SyncFetch {
118121
this.#window.location.protocol === 'https:'
119122
) {
120123
throw new this.#window.DOMException(
121-
`Mixed Content: The page at '${this.#window.location.href}' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint '${this.request.url}'. This request has been blocked; the content must be served over HTTPS.`,
124+
`Mixed Content: The page at '${
125+
this.#window.location.href
126+
}' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint '${
127+
this.request.url
128+
}'. This request has been blocked; the content must be served over HTTPS.`,
122129
DOMExceptionNameEnum.securityError
123130
);
124131
}
@@ -177,7 +184,7 @@ export default class SyncFetch {
177184
url: this.request.url,
178185
init: { headers, method: cachedResponse.request.method },
179186
disableCache: true,
180-
disableCrossOriginPolicy: true
187+
disableSameOriginPolicy: true
181188
});
182189

183190
const validateResponse = <ISyncResponse>fetch.send();
@@ -199,7 +206,7 @@ export default class SyncFetch {
199206
url: this.request.url,
200207
init: { headers, method: cachedResponse.request.method },
201208
disableCache: true,
202-
disableCrossOriginPolicy: true
209+
disableSameOriginPolicy: true
203210
});
204211
fetch.send().then((response) => {
205212
response.buffer().then((body: Buffer) => {
@@ -236,7 +243,7 @@ export default class SyncFetch {
236243
*/
237244
private compliesWithCrossOriginPolicy(): boolean {
238245
if (
239-
this.disableCrossOriginPolicy ||
246+
this.disableSameOriginPolicy ||
240247
!FetchCORSUtility.isCORS(this.#window.location.href, this.request[PropertySymbol.url])
241248
) {
242249
return true;
@@ -288,7 +295,7 @@ export default class SyncFetch {
288295
url: this.request.url,
289296
init: { method: 'OPTIONS' },
290297
disableCache: true,
291-
disableCrossOriginPolicy: true,
298+
disableSameOriginPolicy: true,
292299
unfilteredHeaders: corsHeaders
293300
});
294301

packages/happy-dom/test/fetch/Fetch.test.ts

+64
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,70 @@ describe('Fetch', () => {
571571
});
572572
});
573573

574+
it('Allows cross-origin request if "Browser.settings.fetch.disableSameOriginPolicy" is set to "true".', async () => {
575+
const originURL = 'http://localhost:8080';
576+
const window = new Window({ url: originURL });
577+
const url = 'http://other.origin.com/some/path';
578+
579+
window.happyDOM.settings.fetch.disableSameOriginPolicy = true;
580+
581+
let requestedUrl: string | null = null;
582+
let postRequestHeaders: { [k: string]: string } | null = null;
583+
let optionsRequestHeaders: { [k: string]: string } | null = null;
584+
585+
mockModule('http', {
586+
request: (url, options) => {
587+
requestedUrl = url;
588+
if (options.method === 'OPTIONS') {
589+
optionsRequestHeaders = options.headers;
590+
} else if (options.method === 'POST') {
591+
postRequestHeaders = options.headers;
592+
}
593+
594+
return {
595+
end: () => {},
596+
on: (event: string, callback: (response: HTTP.IncomingMessage) => void) => {
597+
if (event === 'response') {
598+
async function* generate(): AsyncGenerator<string> {}
599+
600+
const response = <HTTP.IncomingMessage>Stream.Readable.from(generate());
601+
602+
response.headers = {};
603+
response.rawHeaders = [];
604+
605+
callback(response);
606+
}
607+
},
608+
setTimeout: () => {}
609+
};
610+
}
611+
});
612+
613+
await window.fetch(url, {
614+
method: 'POST',
615+
body: '{"foo": "bar"}',
616+
headers: {
617+
'X-Custom-Header': 'yes',
618+
'Content-Type': 'application/json'
619+
}
620+
});
621+
622+
expect(requestedUrl).toBe(url);
623+
expect(optionsRequestHeaders).toBeNull();
624+
625+
expect(postRequestHeaders).toEqual({
626+
Accept: '*/*',
627+
Connection: 'close',
628+
'Content-Type': 'application/json',
629+
'Content-Length': '14',
630+
'User-Agent': window.navigator.userAgent,
631+
'Accept-Encoding': 'gzip, deflate, br',
632+
Origin: originURL,
633+
Referer: originURL + '/',
634+
'X-Custom-Header': 'yes'
635+
});
636+
});
637+
574638
for (const httpCode of [301, 302, 303, 307, 308]) {
575639
for (const method of ['GET', 'POST', 'PATCH']) {
576640
it(`Should follow ${method} request redirect code ${httpCode}.`, async () => {

0 commit comments

Comments
 (0)