oidc-spa 8.6.6 → 8.6.7
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.
- package/core/createOidc.js +1 -1
- package/core/tokenExfiltrationDefense.js +169 -143
- package/core/tokenExfiltrationDefense.js.map +1 -1
- package/esm/core/createOidc.js +1 -1
- package/esm/core/tokenExfiltrationDefense.js +169 -143
- package/esm/core/tokenExfiltrationDefense.js.map +1 -1
- package/package.json +1 -1
- package/src/core/tokenExfiltrationDefense.ts +190 -156
|
@@ -33,237 +33,271 @@ function patchFetchApiToSubstituteTokenPlaceholder(params: {
|
|
|
33
33
|
const { resourceServersAllowedHostnames } = params;
|
|
34
34
|
|
|
35
35
|
const fetch_actual = window.fetch;
|
|
36
|
+
//@ts-expect-error
|
|
37
|
+
const fetchLater_actual: typeof fetch_actual | undefined = window.fetchLater;
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
+
const createFetchOrFetchLater = (params: { isFetchLater: boolean }) => {
|
|
40
|
+
const { isFetchLater } = params;
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
const fn: typeof fetch_actual = async (input, init) => {
|
|
43
|
+
const request = input instanceof Request ? input : new Request(input, init);
|
|
41
44
|
|
|
42
|
-
|
|
45
|
+
let didSubstitute = false;
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
const url_before = `${request.url}`;
|
|
47
|
+
let url: string;
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
{
|
|
50
|
+
const url_before = `${request.url}`;
|
|
48
51
|
|
|
49
|
-
|
|
50
|
-
|
|
52
|
+
url = substitutePlaceholderByRealToken(url_before);
|
|
53
|
+
|
|
54
|
+
if (url !== url_before) {
|
|
55
|
+
didSubstitute = true;
|
|
56
|
+
}
|
|
51
57
|
}
|
|
52
|
-
}
|
|
53
58
|
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
prevent_fetching_of_hashed_js_assets: {
|
|
60
|
+
const { pathname } = new URL(url, window.location.href);
|
|
56
61
|
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
if (!viteHashedJsAssetPathRegExp.test(pathname)) {
|
|
63
|
+
break prevent_fetching_of_hashed_js_assets;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
throw new Error("oidc-spa: Blocked request to hashed js static asset.");
|
|
59
67
|
}
|
|
60
68
|
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
const headers = new Headers();
|
|
70
|
+
request.headers.forEach((value, key) => {
|
|
71
|
+
const nextValue = substitutePlaceholderByRealToken(value);
|
|
63
72
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
if (nextValue !== value) {
|
|
74
|
+
didSubstitute = true;
|
|
75
|
+
}
|
|
67
76
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
77
|
+
headers.set(key, nextValue);
|
|
78
|
+
});
|
|
71
79
|
|
|
72
|
-
|
|
73
|
-
});
|
|
80
|
+
let body: BodyInit | undefined;
|
|
74
81
|
|
|
75
|
-
|
|
82
|
+
handle_body: {
|
|
83
|
+
from_init: {
|
|
84
|
+
if (!init) {
|
|
85
|
+
break from_init;
|
|
86
|
+
}
|
|
76
87
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
break from_init;
|
|
81
|
-
}
|
|
88
|
+
if (!init.body) {
|
|
89
|
+
break from_init;
|
|
90
|
+
}
|
|
82
91
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
92
|
+
if (input instanceof Request && input.body !== null) {
|
|
93
|
+
break from_init;
|
|
94
|
+
}
|
|
86
95
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
96
|
+
if (typeof init.body === "string") {
|
|
97
|
+
body = substitutePlaceholderByRealToken(init.body);
|
|
90
98
|
|
|
91
|
-
|
|
92
|
-
|
|
99
|
+
if (init.body !== body) {
|
|
100
|
+
didSubstitute = true;
|
|
101
|
+
}
|
|
93
102
|
|
|
94
|
-
|
|
95
|
-
didSubstitute = true;
|
|
103
|
+
break handle_body;
|
|
96
104
|
}
|
|
97
105
|
|
|
98
|
-
|
|
99
|
-
|
|
106
|
+
if (init.body instanceof URLSearchParams) {
|
|
107
|
+
let didUrlSearchParamsSubstitute = false;
|
|
108
|
+
const next = new URLSearchParams();
|
|
100
109
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
110
|
+
init.body.forEach((value, key) => {
|
|
111
|
+
const nextValue = substitutePlaceholderByRealToken(value);
|
|
112
|
+
|
|
113
|
+
if (nextValue !== value) {
|
|
114
|
+
didUrlSearchParamsSubstitute = true;
|
|
115
|
+
}
|
|
104
116
|
|
|
105
|
-
|
|
106
|
-
|
|
117
|
+
next.append(key, nextValue);
|
|
118
|
+
});
|
|
107
119
|
|
|
108
|
-
if (
|
|
109
|
-
|
|
120
|
+
if (didUrlSearchParamsSubstitute) {
|
|
121
|
+
didSubstitute = true;
|
|
110
122
|
}
|
|
111
123
|
|
|
112
|
-
next.
|
|
113
|
-
});
|
|
124
|
+
body = didUrlSearchParamsSubstitute ? next : init.body;
|
|
114
125
|
|
|
115
|
-
|
|
116
|
-
didSubstitute = true;
|
|
126
|
+
break handle_body;
|
|
117
127
|
}
|
|
118
128
|
|
|
119
|
-
body
|
|
129
|
+
if (init.body instanceof FormData) {
|
|
130
|
+
let didFormDataSubstitute = false;
|
|
131
|
+
const next = new FormData();
|
|
120
132
|
|
|
121
|
-
|
|
122
|
-
|
|
133
|
+
init.body.forEach((value, key) => {
|
|
134
|
+
if (typeof value === "string") {
|
|
135
|
+
const nextValue = substitutePlaceholderByRealToken(value);
|
|
123
136
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
137
|
+
if (nextValue !== value) {
|
|
138
|
+
didFormDataSubstitute = true;
|
|
139
|
+
}
|
|
127
140
|
|
|
128
|
-
|
|
129
|
-
if (typeof value === "string") {
|
|
130
|
-
const nextValue = substitutePlaceholderByRealToken(value);
|
|
141
|
+
next.append(key, nextValue);
|
|
131
142
|
|
|
132
|
-
|
|
133
|
-
didFormDataSubstitute = true;
|
|
143
|
+
return;
|
|
134
144
|
}
|
|
135
145
|
|
|
136
|
-
next.append(key,
|
|
146
|
+
next.append(key, value);
|
|
147
|
+
});
|
|
137
148
|
|
|
138
|
-
|
|
149
|
+
if (didFormDataSubstitute) {
|
|
150
|
+
didSubstitute = true;
|
|
139
151
|
}
|
|
140
152
|
|
|
141
|
-
next.
|
|
142
|
-
});
|
|
153
|
+
body = didFormDataSubstitute ? next : init.body;
|
|
143
154
|
|
|
144
|
-
|
|
145
|
-
didSubstitute = true;
|
|
155
|
+
break handle_body;
|
|
146
156
|
}
|
|
147
157
|
|
|
148
|
-
body
|
|
158
|
+
if (init.body instanceof Blob) {
|
|
159
|
+
break from_init;
|
|
160
|
+
}
|
|
149
161
|
|
|
162
|
+
body = init.body;
|
|
150
163
|
break handle_body;
|
|
151
164
|
}
|
|
152
165
|
|
|
153
|
-
if (
|
|
154
|
-
|
|
166
|
+
if (request.body === null) {
|
|
167
|
+
body = undefined;
|
|
168
|
+
break handle_body;
|
|
155
169
|
}
|
|
156
170
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
171
|
+
const shouldInspectBody = (() => {
|
|
172
|
+
let ct = headers.get("Content-Type");
|
|
160
173
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
174
|
+
if (ct === null) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
165
177
|
|
|
166
|
-
|
|
167
|
-
let ct = headers.get("Content-Type");
|
|
178
|
+
ct = ct.toLocaleLowerCase();
|
|
168
179
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
180
|
+
if (
|
|
181
|
+
!ct.startsWith("application/json") &&
|
|
182
|
+
!ct.startsWith("application/x-www-form-urlencoded")
|
|
183
|
+
) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
172
186
|
|
|
173
|
-
|
|
187
|
+
const len_str = headers.get("Content-Length");
|
|
174
188
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
189
|
+
if (len_str === null) {
|
|
190
|
+
// NOTE: This will have performance implications for large bodies
|
|
191
|
+
// but we have no other way to know the size
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const len = parseInt(len_str, 10);
|
|
181
196
|
|
|
182
|
-
|
|
197
|
+
if (!Number.isFinite(len) || len > 100_000) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
183
200
|
|
|
184
|
-
if (len_str === null) {
|
|
185
|
-
// NOTE: This will have performance implications for large bodies
|
|
186
|
-
// but we have no other way to know the size
|
|
187
201
|
return true;
|
|
202
|
+
})();
|
|
203
|
+
|
|
204
|
+
if (!shouldInspectBody) {
|
|
205
|
+
body = request.body;
|
|
206
|
+
break handle_body;
|
|
188
207
|
}
|
|
189
208
|
|
|
190
|
-
const
|
|
209
|
+
const bodyText = await request.clone().text();
|
|
210
|
+
const nextBodyText = substitutePlaceholderByRealToken(bodyText);
|
|
191
211
|
|
|
192
|
-
if (
|
|
193
|
-
|
|
212
|
+
if (nextBodyText !== bodyText) {
|
|
213
|
+
didSubstitute = true;
|
|
194
214
|
}
|
|
195
215
|
|
|
196
|
-
|
|
197
|
-
})();
|
|
198
|
-
|
|
199
|
-
if (!shouldInspectBody) {
|
|
200
|
-
body = request.body;
|
|
201
|
-
break handle_body;
|
|
216
|
+
body = nextBodyText;
|
|
202
217
|
}
|
|
203
218
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
didSubstitute = true;
|
|
209
|
-
}
|
|
219
|
+
block_authed_request_to_unauthorized_hostnames: {
|
|
220
|
+
if (!didSubstitute) {
|
|
221
|
+
break block_authed_request_to_unauthorized_hostnames;
|
|
222
|
+
}
|
|
210
223
|
|
|
211
|
-
|
|
212
|
-
}
|
|
224
|
+
const { hostname } = new URL(url, window.location.href);
|
|
213
225
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
226
|
+
if (
|
|
227
|
+
getIsHostnameAuthorized({
|
|
228
|
+
allowedHostnames: resourceServersAllowedHostnames,
|
|
229
|
+
extendAuthorizationToParentDomain: true,
|
|
230
|
+
hostname
|
|
231
|
+
})
|
|
232
|
+
) {
|
|
233
|
+
break block_authed_request_to_unauthorized_hostnames;
|
|
234
|
+
}
|
|
218
235
|
|
|
219
|
-
|
|
236
|
+
throw new Error(
|
|
237
|
+
[
|
|
238
|
+
`oidc-spa: Blocked authed request to ${hostname}.`,
|
|
239
|
+
`To authorize this request add "${hostname}" to`,
|
|
240
|
+
"`resourceServersAllowedHostnames`."
|
|
241
|
+
].join(" ")
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const nextInit: RequestInit = {
|
|
246
|
+
method: request.method,
|
|
247
|
+
headers,
|
|
248
|
+
body,
|
|
249
|
+
mode: request.mode,
|
|
250
|
+
credentials: request.credentials,
|
|
251
|
+
cache: request.cache,
|
|
252
|
+
redirect: request.redirect,
|
|
253
|
+
referrer: request.referrer,
|
|
254
|
+
referrerPolicy: request.referrerPolicy,
|
|
255
|
+
integrity: request.integrity,
|
|
256
|
+
keepalive: request.keepalive,
|
|
257
|
+
signal: request.signal
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
{
|
|
261
|
+
//@ts-expect-error
|
|
262
|
+
const duplex = init?.duplex ?? (input instanceof Request ? input.duplex : undefined);
|
|
220
263
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
hostname
|
|
226
|
-
})
|
|
227
|
-
) {
|
|
228
|
-
break block_authed_request_to_unauthorized_hostnames;
|
|
264
|
+
if (duplex !== undefined) {
|
|
265
|
+
//@ts-expect-error
|
|
266
|
+
nextInit.duplex = duplex;
|
|
267
|
+
}
|
|
229
268
|
}
|
|
230
269
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
"`resourceServersAllowedHostnames`."
|
|
236
|
-
].join(" ")
|
|
237
|
-
);
|
|
238
|
-
}
|
|
270
|
+
fetch_later: {
|
|
271
|
+
if (!isFetchLater) {
|
|
272
|
+
break fetch_later;
|
|
273
|
+
}
|
|
239
274
|
|
|
240
|
-
|
|
241
|
-
method: request.method,
|
|
242
|
-
headers,
|
|
243
|
-
body,
|
|
244
|
-
mode: request.mode,
|
|
245
|
-
credentials: request.credentials,
|
|
246
|
-
cache: request.cache,
|
|
247
|
-
redirect: request.redirect,
|
|
248
|
-
referrer: request.referrer,
|
|
249
|
-
referrerPolicy: request.referrerPolicy,
|
|
250
|
-
integrity: request.integrity,
|
|
251
|
-
keepalive: request.keepalive,
|
|
252
|
-
signal: request.signal
|
|
253
|
-
};
|
|
275
|
+
assert(fetchLater_actual !== undefined);
|
|
254
276
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
277
|
+
const activateAfter =
|
|
278
|
+
//@ts-expect-error
|
|
279
|
+
init?.activateAfter ?? (input instanceof Request ? input.activateAfter : undefined);
|
|
258
280
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
281
|
+
if (activateAfter !== undefined) {
|
|
282
|
+
//@ts-expect-error
|
|
283
|
+
nextInit.activateAfter = activateAfter;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return fetchLater_actual(url, nextInit);
|
|
262
287
|
}
|
|
263
|
-
}
|
|
264
288
|
|
|
265
|
-
|
|
289
|
+
return fetch_actual(url, nextInit);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
return fn;
|
|
266
293
|
};
|
|
294
|
+
|
|
295
|
+
window.fetch = createFetchOrFetchLater({ isFetchLater: false });
|
|
296
|
+
// @ts-expect-error
|
|
297
|
+
if (window.fetchLater) {
|
|
298
|
+
// @ts-expect-error
|
|
299
|
+
window.fetchLater = createFetchOrFetchLater({ isFetchLater: true });
|
|
300
|
+
}
|
|
267
301
|
}
|
|
268
302
|
|
|
269
303
|
function patchXMLHttpRequestApiToSubstituteTokenPlaceholder(params: {
|