oidc-spa 8.4.8 → 8.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +2 -5
  2. package/core/createOidc.js +3 -1
  3. package/core/createOidc.js.map +1 -1
  4. package/core/earlyInit.d.ts +45 -7
  5. package/core/earlyInit.js +69 -153
  6. package/core/earlyInit.js.map +1 -1
  7. package/core/oidcClientTsUserToTokens.d.ts +1 -0
  8. package/core/oidcClientTsUserToTokens.js +11 -1
  9. package/core/oidcClientTsUserToTokens.js.map +1 -1
  10. package/core/tokenExfiltrationDefense.d.ts +6 -0
  11. package/core/tokenExfiltrationDefense.js +607 -0
  12. package/core/tokenExfiltrationDefense.js.map +1 -0
  13. package/core/tokenExfiltrationDefense_legacy.d.ts +8 -0
  14. package/core/tokenExfiltrationDefense_legacy.js +133 -0
  15. package/core/tokenExfiltrationDefense_legacy.js.map +1 -0
  16. package/core/tokenPlaceholderSubstitution.d.ts +13 -0
  17. package/core/tokenPlaceholderSubstitution.js +79 -0
  18. package/core/tokenPlaceholderSubstitution.js.map +1 -0
  19. package/esm/core/createOidc.js +3 -1
  20. package/esm/core/createOidc.js.map +1 -1
  21. package/esm/core/earlyInit.d.ts +45 -7
  22. package/esm/core/earlyInit.js +69 -153
  23. package/esm/core/earlyInit.js.map +1 -1
  24. package/esm/core/oidcClientTsUserToTokens.d.ts +1 -0
  25. package/esm/core/oidcClientTsUserToTokens.js +11 -1
  26. package/esm/core/oidcClientTsUserToTokens.js.map +1 -1
  27. package/esm/core/tokenExfiltrationDefense.d.ts +6 -0
  28. package/esm/core/tokenExfiltrationDefense.js +604 -0
  29. package/esm/core/tokenExfiltrationDefense.js.map +1 -0
  30. package/esm/core/tokenExfiltrationDefense_legacy.d.ts +8 -0
  31. package/esm/core/tokenExfiltrationDefense_legacy.js +130 -0
  32. package/esm/core/tokenExfiltrationDefense_legacy.js.map +1 -0
  33. package/esm/core/tokenPlaceholderSubstitution.d.ts +13 -0
  34. package/esm/core/tokenPlaceholderSubstitution.js +73 -0
  35. package/esm/core/tokenPlaceholderSubstitution.js.map +1 -0
  36. package/esm/tools/isDomain.d.ts +1 -0
  37. package/esm/tools/isDomain.js +16 -0
  38. package/esm/tools/isDomain.js.map +1 -0
  39. package/esm/tools/isHostnameAuthorized.d.ts +5 -0
  40. package/esm/tools/isHostnameAuthorized.js +74 -0
  41. package/esm/tools/isHostnameAuthorized.js.map +1 -0
  42. package/esm/tools/isLikelyDevServer.js +18 -10
  43. package/esm/tools/isLikelyDevServer.js.map +1 -1
  44. package/package.json +1 -1
  45. package/src/core/createOidc.ts +2 -0
  46. package/src/core/earlyInit.ts +138 -192
  47. package/src/core/oidcClientTsUserToTokens.ts +14 -0
  48. package/src/core/tokenExfiltrationDefense.ts +862 -0
  49. package/src/core/tokenExfiltrationDefense_legacy.ts +165 -0
  50. package/src/core/tokenPlaceholderSubstitution.ts +105 -0
  51. package/src/tools/isDomain.ts +18 -0
  52. package/src/tools/isHostnameAuthorized.ts +91 -0
  53. package/src/tools/isLikelyDevServer.ts +23 -11
  54. package/src/vite-plugin/handleClientEntrypoint.ts +57 -20
  55. package/src/vite-plugin/vite-plugin.ts +5 -10
  56. package/tools/isDomain.d.ts +1 -0
  57. package/tools/isDomain.js +19 -0
  58. package/tools/isDomain.js.map +1 -0
  59. package/tools/isHostnameAuthorized.d.ts +5 -0
  60. package/tools/isHostnameAuthorized.js +77 -0
  61. package/tools/isHostnameAuthorized.js.map +1 -0
  62. package/tools/isLikelyDevServer.js +18 -10
  63. package/tools/isLikelyDevServer.js.map +1 -1
  64. package/vite-plugin/handleClientEntrypoint.js +36 -17
  65. package/vite-plugin/handleClientEntrypoint.js.map +1 -1
  66. package/vite-plugin/vite-plugin.d.ts +3 -4
  67. package/vite-plugin/vite-plugin.js +1 -5
  68. package/vite-plugin/vite-plugin.js.map +1 -1
@@ -0,0 +1,862 @@
1
+ import { assert } from "../tools/tsafe/assert";
2
+ import {
3
+ markTokenSubstitutionAdEnabled,
4
+ substitutePlaceholderByRealToken
5
+ } from "./tokenPlaceholderSubstitution";
6
+ import { getIsHostnameAuthorized } from "../tools/isHostnameAuthorized";
7
+
8
+ type Params = {
9
+ resourceServersAllowedHostnames: string[] | undefined;
10
+ serviceWorkersAllowedHostnames: string[] | undefined;
11
+ };
12
+
13
+ const viteHashedJsAssetPathRegExp = /\/assets\/[^/]+-[a-zA-Z0-9_-]{8}\.js$/;
14
+
15
+ export function enableTokenExfiltrationDefense(params: Params) {
16
+ const { resourceServersAllowedHostnames = [], serviceWorkersAllowedHostnames = [] } = params;
17
+
18
+ markTokenSubstitutionAdEnabled();
19
+
20
+ patchFetchApiToSubstituteTokenPlaceholder({ resourceServersAllowedHostnames });
21
+ patchXMLHttpRequestApiToSubstituteTokenPlaceholder({ resourceServersAllowedHostnames });
22
+ patchWebSocketApiToSubstituteTokenPlaceholder({ resourceServersAllowedHostnames });
23
+ patchEventSourceApiToSubstituteTokenPlaceholder({ resourceServersAllowedHostnames });
24
+ patchNavigatorSendBeaconApiToSubstituteTokenPlaceholder({ resourceServersAllowedHostnames });
25
+ restrictServiceWorkerRegistration({ serviceWorkersAllowedHostnames });
26
+
27
+ runMonkeyPatchingPrevention();
28
+ }
29
+
30
+ function patchFetchApiToSubstituteTokenPlaceholder(params: {
31
+ resourceServersAllowedHostnames: string[];
32
+ }) {
33
+ const { resourceServersAllowedHostnames } = params;
34
+
35
+ const fetch_actual = window.fetch;
36
+
37
+ window.fetch = async function fetch(input, init) {
38
+ const request = input instanceof Request ? input : new Request(input, init);
39
+
40
+ prevent_fetching_of_hashed_js_assets: {
41
+ const { pathname } = new URL(request.url, window.location.href);
42
+
43
+ if (!viteHashedJsAssetPathRegExp.test(pathname)) {
44
+ break prevent_fetching_of_hashed_js_assets;
45
+ }
46
+
47
+ throw new Error("oidc-spa: Blocked request to hashed js static asset.");
48
+ }
49
+
50
+ let didSubstitute = false;
51
+
52
+ const headers = new Headers();
53
+ request.headers.forEach((value, key) => {
54
+ const nextValue = substitutePlaceholderByRealToken(value);
55
+
56
+ if (nextValue !== value) {
57
+ didSubstitute = true;
58
+ }
59
+
60
+ headers.set(key, nextValue);
61
+ });
62
+
63
+ let body: BodyInit | undefined;
64
+
65
+ handle_body: {
66
+ from_init: {
67
+ if (!init) {
68
+ break from_init;
69
+ }
70
+
71
+ if (!init.body) {
72
+ break from_init;
73
+ }
74
+
75
+ if (input instanceof Request && input.body !== null) {
76
+ break from_init;
77
+ }
78
+
79
+ if (typeof init.body === "string") {
80
+ body = substitutePlaceholderByRealToken(init.body);
81
+
82
+ if (init.body !== body) {
83
+ didSubstitute = true;
84
+ }
85
+
86
+ break handle_body;
87
+ }
88
+
89
+ if (init.body instanceof URLSearchParams) {
90
+ let didUrlSearchParamsSubstitute = false;
91
+ const next = new URLSearchParams();
92
+
93
+ init.body.forEach((value, key) => {
94
+ const nextValue = substitutePlaceholderByRealToken(value);
95
+
96
+ if (nextValue !== value) {
97
+ didUrlSearchParamsSubstitute = true;
98
+ }
99
+
100
+ next.append(key, nextValue);
101
+ });
102
+
103
+ if (didUrlSearchParamsSubstitute) {
104
+ didSubstitute = true;
105
+ }
106
+
107
+ body = didUrlSearchParamsSubstitute ? next : init.body;
108
+
109
+ break handle_body;
110
+ }
111
+
112
+ if (init.body instanceof FormData) {
113
+ let didFormDataSubstitute = false;
114
+ const next = new FormData();
115
+
116
+ init.body.forEach((value, key) => {
117
+ if (typeof value === "string") {
118
+ const nextValue = substitutePlaceholderByRealToken(value);
119
+
120
+ if (nextValue !== value) {
121
+ didFormDataSubstitute = true;
122
+ }
123
+
124
+ next.append(key, nextValue);
125
+
126
+ return;
127
+ }
128
+
129
+ next.append(key, value);
130
+ });
131
+
132
+ if (didFormDataSubstitute) {
133
+ didSubstitute = true;
134
+ }
135
+
136
+ body = didFormDataSubstitute ? next : init.body;
137
+
138
+ break handle_body;
139
+ }
140
+
141
+ if (init.body instanceof Blob) {
142
+ break from_init;
143
+ }
144
+
145
+ body = init.body;
146
+ break handle_body;
147
+ }
148
+
149
+ if (request.body === null) {
150
+ body = undefined;
151
+ break handle_body;
152
+ }
153
+
154
+ const shouldInspectBody = (() => {
155
+ let ct = request.headers.get("Content-Type");
156
+
157
+ if (ct === null) {
158
+ return false;
159
+ }
160
+
161
+ ct = ct.toLocaleLowerCase();
162
+
163
+ if (
164
+ !ct.startsWith("application/json") &&
165
+ !ct.startsWith("application/x-www-form-urlencoded")
166
+ ) {
167
+ return false;
168
+ }
169
+
170
+ const len_str = request.headers.get("Content-Length");
171
+
172
+ if (!len_str) {
173
+ return false;
174
+ }
175
+
176
+ const len = parseInt(len_str, 10);
177
+
178
+ if (!Number.isFinite(len) || len > 100_000) {
179
+ return false;
180
+ }
181
+
182
+ return true;
183
+ })();
184
+
185
+ if (!shouldInspectBody) {
186
+ body = request.body;
187
+ break handle_body;
188
+ }
189
+
190
+ const bodyText = await request.clone().text();
191
+ const nextBodyText = substitutePlaceholderByRealToken(bodyText);
192
+
193
+ if (nextBodyText !== bodyText) {
194
+ didSubstitute = true;
195
+ }
196
+
197
+ body = nextBodyText;
198
+ }
199
+
200
+ block_authed_request_to_unauthorized_hostnames: {
201
+ if (!didSubstitute) {
202
+ break block_authed_request_to_unauthorized_hostnames;
203
+ }
204
+
205
+ const { hostname } = new URL(request.url, window.location.href);
206
+
207
+ if (
208
+ getIsHostnameAuthorized({
209
+ allowedHostnames: resourceServersAllowedHostnames,
210
+ extendAuthorizationToParentDomain: true,
211
+ hostname
212
+ })
213
+ ) {
214
+ break block_authed_request_to_unauthorized_hostnames;
215
+ }
216
+
217
+ throw new Error(
218
+ [
219
+ `oidc-spa: Blocked authed request to ${hostname}.`,
220
+ `To authorize this request add "${hostname}" to`,
221
+ "`resourceServersAllowedHostnames`."
222
+ ].join(" ")
223
+ );
224
+ }
225
+
226
+ return fetch_actual(request.url, {
227
+ method: request.method,
228
+ headers,
229
+ body,
230
+ mode: request.mode,
231
+ credentials: request.credentials,
232
+ cache: request.cache,
233
+ redirect: request.redirect,
234
+ referrer: request.referrer,
235
+ referrerPolicy: request.referrerPolicy,
236
+ integrity: request.integrity,
237
+ keepalive: request.keepalive,
238
+ signal: request.signal
239
+ });
240
+ };
241
+ }
242
+
243
+ function patchXMLHttpRequestApiToSubstituteTokenPlaceholder(params: {
244
+ resourceServersAllowedHostnames: string[];
245
+ }) {
246
+ const { resourceServersAllowedHostnames } = params;
247
+
248
+ const open_actual = XMLHttpRequest.prototype.open;
249
+ const send_actual = XMLHttpRequest.prototype.send;
250
+ const setRequestHeader_actual = XMLHttpRequest.prototype.setRequestHeader;
251
+
252
+ type XhrData = {
253
+ url: string;
254
+ didSubstitute: boolean;
255
+ };
256
+
257
+ const xhrDataSymbol = Symbol("oidc-spa XMLHttpRequest data");
258
+
259
+ const getXhrData = (xhr: XMLHttpRequest): XhrData => {
260
+ const xhr_any = xhr as any;
261
+
262
+ if (xhr_any[xhrDataSymbol] !== undefined) {
263
+ return xhr_any[xhrDataSymbol];
264
+ }
265
+
266
+ const data: XhrData = {
267
+ url: "",
268
+ didSubstitute: false
269
+ };
270
+
271
+ xhr_any[xhrDataSymbol] = data;
272
+
273
+ return data;
274
+ };
275
+
276
+ XMLHttpRequest.prototype.open = function open(
277
+ method: string,
278
+ url: string | URL,
279
+ async?: boolean,
280
+ username?: string | null,
281
+ password?: string | null
282
+ ) {
283
+ const xhrData = getXhrData(this);
284
+
285
+ xhrData.url = typeof url === "string" ? url : url.href;
286
+ xhrData.didSubstitute = false;
287
+
288
+ if (async === undefined) {
289
+ return open_actual.bind(this)(method, url);
290
+ } else {
291
+ return open_actual.call(this, method, url, async, username, password);
292
+ }
293
+ };
294
+
295
+ XMLHttpRequest.prototype.setRequestHeader = function setRequestHeader(name, value) {
296
+ const xhrData = getXhrData(this);
297
+ const nextValue = substitutePlaceholderByRealToken(value);
298
+
299
+ if (nextValue !== value) {
300
+ xhrData.didSubstitute = true;
301
+ }
302
+
303
+ return setRequestHeader_actual.call(this, name, nextValue);
304
+ };
305
+
306
+ XMLHttpRequest.prototype.send = function send(body) {
307
+ const xhrData = getXhrData(this);
308
+
309
+ prevent_fetching_of_hashed_js_assets: {
310
+ const { pathname } = new URL(xhrData.url, window.location.href);
311
+
312
+ if (!viteHashedJsAssetPathRegExp.test(pathname)) {
313
+ break prevent_fetching_of_hashed_js_assets;
314
+ }
315
+
316
+ throw new Error("oidc-spa: Blocked request to hashed static asset.");
317
+ }
318
+
319
+ let nextBody = body;
320
+
321
+ if (typeof body === "string") {
322
+ const nextBodyText = substitutePlaceholderByRealToken(body);
323
+
324
+ if (nextBodyText !== body) {
325
+ xhrData.didSubstitute = true;
326
+ }
327
+
328
+ nextBody = nextBodyText;
329
+ }
330
+
331
+ block_authed_request_to_unauthorized_hostnames: {
332
+ if (!xhrData.didSubstitute) {
333
+ break block_authed_request_to_unauthorized_hostnames;
334
+ }
335
+
336
+ const { hostname } = new URL(xhrData.url, window.location.href);
337
+
338
+ if (
339
+ getIsHostnameAuthorized({
340
+ allowedHostnames: resourceServersAllowedHostnames,
341
+ extendAuthorizationToParentDomain: true,
342
+ hostname
343
+ })
344
+ ) {
345
+ break block_authed_request_to_unauthorized_hostnames;
346
+ }
347
+
348
+ throw new Error(
349
+ [
350
+ `oidc-spa: Blocked authed request to ${hostname}.`,
351
+ `To authorize this request add "${hostname}" to`,
352
+ "`resourceServersAllowedHostnames`."
353
+ ].join(" ")
354
+ );
355
+ }
356
+
357
+ return send_actual.call(this, nextBody as Parameters<XMLHttpRequest["send"]>[0]);
358
+ };
359
+ }
360
+
361
+ function patchWebSocketApiToSubstituteTokenPlaceholder(params: {
362
+ resourceServersAllowedHostnames: string[];
363
+ }) {
364
+ const { resourceServersAllowedHostnames } = params;
365
+
366
+ const WebSocket_actual = window.WebSocket;
367
+ const send_actual = WebSocket_actual.prototype.send;
368
+
369
+ type WsData = {
370
+ url: string;
371
+ hostname: string;
372
+ pathname: string;
373
+ didSubstitute: boolean;
374
+ };
375
+
376
+ const wsDataByWs = new WeakMap<WebSocket, WsData>();
377
+
378
+ const WebSocketPatched = function WebSocket(url: string | URL, protocols?: string | string[]) {
379
+ const urlStr = typeof url === "string" ? url : url.href;
380
+ const nextUrl = substitutePlaceholderByRealToken(urlStr);
381
+ let didSubstitute = nextUrl !== urlStr;
382
+
383
+ const nextProtocols = (() => {
384
+ if (protocols === undefined) {
385
+ return protocols;
386
+ }
387
+
388
+ if (typeof protocols === "string") {
389
+ const next = substitutePlaceholderByRealToken(protocols);
390
+
391
+ if (next !== protocols) {
392
+ didSubstitute = true;
393
+ }
394
+
395
+ return next;
396
+ }
397
+
398
+ let didProtocolsSubstitute = false;
399
+
400
+ const next = protocols.map(protocol => {
401
+ const nextProtocol = substitutePlaceholderByRealToken(protocol);
402
+
403
+ if (nextProtocol !== protocol) {
404
+ didProtocolsSubstitute = true;
405
+ }
406
+
407
+ return nextProtocol;
408
+ });
409
+
410
+ if (didProtocolsSubstitute) {
411
+ didSubstitute = true;
412
+ }
413
+
414
+ return next;
415
+ })();
416
+
417
+ const { hostname, pathname } = new URL(nextUrl, window.location.href);
418
+
419
+ block_authed_request_to_unauthorized_hostnames: {
420
+ if (!didSubstitute) {
421
+ break block_authed_request_to_unauthorized_hostnames;
422
+ }
423
+
424
+ if (
425
+ getIsHostnameAuthorized({
426
+ allowedHostnames: resourceServersAllowedHostnames,
427
+ extendAuthorizationToParentDomain: true,
428
+ hostname
429
+ })
430
+ ) {
431
+ break block_authed_request_to_unauthorized_hostnames;
432
+ }
433
+
434
+ throw new Error(
435
+ [
436
+ `oidc-spa: Blocked authed request to ${hostname}.`,
437
+ `To authorize this request add "${hostname}" to`,
438
+ "`resourceServersAllowedHostnames`."
439
+ ].join(" ")
440
+ );
441
+ }
442
+
443
+ const ws = new WebSocket_actual(nextUrl, nextProtocols as Parameters<typeof WebSocket>[1]);
444
+
445
+ wsDataByWs.set(ws, {
446
+ url: nextUrl,
447
+ hostname,
448
+ pathname,
449
+ didSubstitute
450
+ });
451
+
452
+ return ws;
453
+ };
454
+
455
+ WebSocketPatched.prototype = WebSocket_actual.prototype;
456
+
457
+ for (const name of ["CONNECTING", "OPEN", "CLOSING", "CLOSED"] as const) {
458
+ Object.defineProperty(WebSocketPatched, name, {
459
+ value: WebSocket_actual[name],
460
+ writable: false,
461
+ enumerable: true,
462
+ configurable: false
463
+ });
464
+ }
465
+
466
+ window.WebSocket = WebSocketPatched as unknown as typeof WebSocket;
467
+
468
+ WebSocket_actual.prototype.send = function send(data) {
469
+ const wsData = wsDataByWs.get(this);
470
+
471
+ if (wsData === undefined) {
472
+ // NOTE: This can happen for Vite's dev server websocket
473
+ return send_actual.call(this, data);
474
+ }
475
+
476
+ let nextData = data;
477
+
478
+ if (typeof data === "string") {
479
+ const nextDataText = substitutePlaceholderByRealToken(data);
480
+
481
+ if (nextDataText !== data) {
482
+ wsData.didSubstitute = true;
483
+ }
484
+
485
+ nextData = nextDataText;
486
+ }
487
+
488
+ block_authed_request_to_unauthorized_hostnames: {
489
+ if (!wsData.didSubstitute) {
490
+ break block_authed_request_to_unauthorized_hostnames;
491
+ }
492
+
493
+ if (
494
+ getIsHostnameAuthorized({
495
+ allowedHostnames: resourceServersAllowedHostnames,
496
+ extendAuthorizationToParentDomain: true,
497
+ hostname: wsData.hostname
498
+ })
499
+ ) {
500
+ break block_authed_request_to_unauthorized_hostnames;
501
+ }
502
+
503
+ throw new Error(
504
+ [
505
+ `oidc-spa: Blocked authed request to ${wsData.hostname}.`,
506
+ `To authorize this request add "${wsData.hostname}" to`,
507
+ "`resourceServersAllowedHostnames`."
508
+ ].join(" ")
509
+ );
510
+ }
511
+
512
+ prevent_fetching_of_hashed_js_assets: {
513
+ if (!viteHashedJsAssetPathRegExp.test(wsData.pathname)) {
514
+ break prevent_fetching_of_hashed_js_assets;
515
+ }
516
+
517
+ throw new Error("oidc-spa: Blocked request to hashed static asset.");
518
+ }
519
+
520
+ return send_actual.call(this, nextData);
521
+ };
522
+ }
523
+
524
+ function patchEventSourceApiToSubstituteTokenPlaceholder(params: {
525
+ resourceServersAllowedHostnames: string[];
526
+ }) {
527
+ const { resourceServersAllowedHostnames } = params;
528
+
529
+ const EventSource_actual = window.EventSource;
530
+
531
+ if (EventSource_actual === undefined) {
532
+ return;
533
+ }
534
+
535
+ const EventSourcePatched = function EventSource(
536
+ url: string | URL,
537
+ eventSourceInitDict?: EventSourceInit
538
+ ) {
539
+ const urlStr = typeof url === "string" ? url : url.href;
540
+ const nextUrl = substitutePlaceholderByRealToken(urlStr);
541
+ const didSubstitute = nextUrl !== urlStr;
542
+
543
+ const { hostname } = new URL(nextUrl, window.location.href);
544
+
545
+ block_authed_request_to_unauthorized_hostnames: {
546
+ if (!didSubstitute) {
547
+ break block_authed_request_to_unauthorized_hostnames;
548
+ }
549
+
550
+ if (
551
+ getIsHostnameAuthorized({
552
+ allowedHostnames: resourceServersAllowedHostnames,
553
+ extendAuthorizationToParentDomain: true,
554
+ hostname
555
+ })
556
+ ) {
557
+ break block_authed_request_to_unauthorized_hostnames;
558
+ }
559
+
560
+ throw new Error(
561
+ [
562
+ `oidc-spa: Blocked authed request to ${hostname}.`,
563
+ `To authorize this request add "${hostname}" to`,
564
+ "`resourceServersAllowedHostnames`."
565
+ ].join(" ")
566
+ );
567
+ }
568
+
569
+ return new EventSource_actual(nextUrl, eventSourceInitDict);
570
+ };
571
+
572
+ EventSourcePatched.prototype = EventSource_actual.prototype;
573
+
574
+ if ("CONNECTING" in EventSource_actual) {
575
+ for (const name of ["CONNECTING", "OPEN", "CLOSED"] as const) {
576
+ Object.defineProperty(EventSourcePatched, name, {
577
+ value: (EventSource_actual as any)[name],
578
+ writable: false,
579
+ enumerable: true,
580
+ configurable: false
581
+ });
582
+ }
583
+ }
584
+
585
+ window.EventSource = EventSourcePatched as unknown as typeof EventSource;
586
+ }
587
+
588
+ function patchNavigatorSendBeaconApiToSubstituteTokenPlaceholder(params: {
589
+ resourceServersAllowedHostnames: string[];
590
+ }) {
591
+ const { resourceServersAllowedHostnames } = params;
592
+
593
+ const sendBeacon_actual = navigator.sendBeacon?.bind(navigator);
594
+
595
+ if (sendBeacon_actual === undefined) {
596
+ return;
597
+ }
598
+
599
+ navigator.sendBeacon = function sendBeacon(url: string | URL, data?: BodyInit | null) {
600
+ const urlStr = typeof url === "string" ? url : url.href;
601
+ const nextUrl = substitutePlaceholderByRealToken(urlStr);
602
+ let didSubstitute = nextUrl !== urlStr;
603
+
604
+ const { hostname } = new URL(nextUrl, window.location.href);
605
+
606
+ let nextData = data;
607
+
608
+ if (typeof data === "string") {
609
+ const next = substitutePlaceholderByRealToken(data);
610
+
611
+ if (next !== data) {
612
+ didSubstitute = true;
613
+ }
614
+
615
+ nextData = next;
616
+ } else if (data instanceof URLSearchParams) {
617
+ let didUrlSearchParamsSubstitute = false;
618
+ const next = new URLSearchParams();
619
+
620
+ data.forEach((value, key) => {
621
+ const nextValue = substitutePlaceholderByRealToken(value);
622
+
623
+ if (nextValue !== value) {
624
+ didUrlSearchParamsSubstitute = true;
625
+ }
626
+
627
+ next.append(key, nextValue);
628
+ });
629
+
630
+ if (didUrlSearchParamsSubstitute) {
631
+ didSubstitute = true;
632
+ nextData = next;
633
+ }
634
+ } else if (data instanceof FormData) {
635
+ let didFormDataSubstitute = false;
636
+ const next = new FormData();
637
+
638
+ data.forEach((value, key) => {
639
+ if (typeof value === "string") {
640
+ const nextValue = substitutePlaceholderByRealToken(value);
641
+
642
+ if (nextValue !== value) {
643
+ didFormDataSubstitute = true;
644
+ }
645
+
646
+ next.append(key, nextValue);
647
+
648
+ return;
649
+ }
650
+
651
+ next.append(key, value);
652
+ });
653
+
654
+ if (didFormDataSubstitute) {
655
+ didSubstitute = true;
656
+ nextData = next;
657
+ }
658
+ }
659
+
660
+ block_authed_request_to_unauthorized_hostnames: {
661
+ if (!didSubstitute) {
662
+ break block_authed_request_to_unauthorized_hostnames;
663
+ }
664
+
665
+ if (
666
+ getIsHostnameAuthorized({
667
+ allowedHostnames: resourceServersAllowedHostnames,
668
+ extendAuthorizationToParentDomain: true,
669
+ hostname
670
+ })
671
+ ) {
672
+ break block_authed_request_to_unauthorized_hostnames;
673
+ }
674
+
675
+ throw new Error(
676
+ [
677
+ `oidc-spa: Blocked authed request to ${hostname}.`,
678
+ `To authorize this request add "${hostname}" to`,
679
+ "`resourceServersAllowedHostnames`."
680
+ ].join(" ")
681
+ );
682
+ }
683
+
684
+ return sendBeacon_actual(nextUrl, nextData as Parameters<typeof navigator.sendBeacon>[1]);
685
+ };
686
+ }
687
+
688
+ function runMonkeyPatchingPrevention() {
689
+ const createWriteError = (target: string) =>
690
+ new Error(
691
+ [
692
+ `oidc-spa: Monkey patching of ${target} has been blocked.`,
693
+ `Read: https://docs.oidc-spa.dev/v/v8/resources/blocked-monkey-patching`
694
+ ].join(" ")
695
+ );
696
+
697
+ for (const name of [
698
+ "fetch",
699
+ "XMLHttpRequest",
700
+ "WebSocket",
701
+ "Headers",
702
+ "URLSearchParams",
703
+ "EventSource",
704
+ "ServiceWorkerContainer",
705
+ "ServiceWorkerRegistration",
706
+ "ServiceWorker",
707
+ "FormData",
708
+ "Blob",
709
+ "String",
710
+ "Object",
711
+ "Promise",
712
+ "Array",
713
+ "RegExp",
714
+ "TextEncoder",
715
+ "Uint8Array",
716
+ "Uint32Array",
717
+ "Response",
718
+ "Reflect",
719
+ "JSON",
720
+ "encodeURIComponent",
721
+ "decodeURIComponent",
722
+ "atob",
723
+ "btoa"
724
+ ] as const) {
725
+ const original = window[name];
726
+
727
+ if (!original) {
728
+ continue;
729
+ }
730
+
731
+ if ("prototype" in original) {
732
+ for (const propertyName of Object.getOwnPropertyNames(original.prototype)) {
733
+ if (name === "Object") {
734
+ if (
735
+ propertyName === "toString" ||
736
+ propertyName === "constructor" ||
737
+ propertyName === "valueOf"
738
+ ) {
739
+ continue;
740
+ }
741
+ }
742
+
743
+ if (name === "Array") {
744
+ if (propertyName === "constructor" || propertyName === "concat") {
745
+ continue;
746
+ }
747
+ }
748
+
749
+ const pd = Object.getOwnPropertyDescriptor(original.prototype, propertyName);
750
+
751
+ assert(pd !== undefined);
752
+
753
+ if (!pd.configurable) {
754
+ continue;
755
+ }
756
+
757
+ Object.defineProperty(original.prototype, propertyName, {
758
+ enumerable: pd.enumerable,
759
+ configurable: false,
760
+ ...("value" in pd
761
+ ? {
762
+ get: () => pd.value,
763
+ set: () => {
764
+ throw createWriteError(`window.${name}.prototype.${propertyName}`);
765
+ }
766
+ }
767
+ : {
768
+ get: pd.get,
769
+ set:
770
+ pd.set ??
771
+ (() => {
772
+ throw createWriteError(`window.${name}.prototype.${propertyName}`);
773
+ })
774
+ })
775
+ });
776
+ }
777
+ }
778
+
779
+ Object.defineProperty(window, name, {
780
+ configurable: false,
781
+ enumerable: true,
782
+ get: () => original,
783
+ set: () => {
784
+ throw createWriteError(`window.${name}`);
785
+ }
786
+ });
787
+ }
788
+
789
+ {
790
+ const name = "serviceWorker";
791
+
792
+ const original = navigator[name];
793
+
794
+ Object.defineProperty(navigator, name, {
795
+ configurable: false,
796
+ enumerable: true,
797
+ get: () => original,
798
+ set: () => {
799
+ throw createWriteError(`window.navigator.${name}`);
800
+ }
801
+ });
802
+ }
803
+
804
+ for (const name of ["call", "apply", "bind"] as const) {
805
+ const original = Function.prototype[name];
806
+
807
+ Object.defineProperty(Function.prototype, name, {
808
+ configurable: false,
809
+ enumerable: true,
810
+ get: () => original,
811
+ set: () => {
812
+ throw createWriteError(`window.Function.prototype.${name})`);
813
+ }
814
+ });
815
+ }
816
+ }
817
+
818
+ function restrictServiceWorkerRegistration(params: { serviceWorkersAllowedHostnames: string[] }) {
819
+ const { serviceWorkersAllowedHostnames } = params;
820
+
821
+ const { serviceWorker } = navigator;
822
+
823
+ const register_actual = serviceWorker.register.bind(serviceWorker);
824
+
825
+ serviceWorker.register = function register(
826
+ scriptURL: Parameters<ServiceWorkerContainer["register"]>[0],
827
+ options?: Parameters<ServiceWorkerContainer["register"]>[1]
828
+ ) {
829
+ const { hostname, protocol } = new URL(
830
+ typeof scriptURL === "string" ? scriptURL : scriptURL.href,
831
+ window.location.href
832
+ );
833
+
834
+ if (protocol === "blob:") {
835
+ throw new Error(
836
+ [
837
+ "oidc-spa: Blocked service worker registration from blob.",
838
+ "Only solution: Set enableTokenExfiltrationDefense to false",
839
+ "or load the worker script from a remote url."
840
+ ].join(" ")
841
+ );
842
+ }
843
+
844
+ if (
845
+ !getIsHostnameAuthorized({
846
+ allowedHostnames: serviceWorkersAllowedHostnames,
847
+ extendAuthorizationToParentDomain: false,
848
+ hostname
849
+ })
850
+ ) {
851
+ throw new Error(
852
+ [
853
+ `oidc-spa: Blocked service worker registration to ${hostname}.`,
854
+ `To authorize this registration add "${hostname}" to`,
855
+ "`serviceWorkersAllowedHostnames`."
856
+ ].join(" ")
857
+ );
858
+ }
859
+
860
+ return register_actual(scriptURL, options);
861
+ };
862
+ }