soiz1 commited on
Commit
f3655e1
·
verified ·
1 Parent(s): 2fba335

Update src/containers/tw-security-manager.jsx

Browse files
Files changed (1) hide show
  1. src/containers/tw-security-manager.jsx +180 -43
src/containers/tw-security-manager.jsx CHANGED
@@ -5,6 +5,7 @@ import log from '../lib/log';
5
  import bindAll from 'lodash.bindall';
6
  import SecurityManagerModal from '../components/tw-security-manager-modal/security-manager-modal.jsx';
7
  import SecurityModals from '../lib/tw-security-manager-constants';
 
8
 
9
  /**
10
  * Set of extension URLs that the user has manually trusted to load unsandboxed.
@@ -31,7 +32,7 @@ const isTrustedExtensionOrigin = url => (
31
  url.startsWith('https://pen-group.github.io/') || // Pen-Group / ObviousAlexC
32
 
33
  // For development.
34
- true ||
35
  url.startsWith('http://localhost:6000') || // Launcher Home
36
  url.startsWith('http://localhost:6001') || // Launcher Extensions
37
  url.startsWith('http://localhost:5173') || // Local Home or Extensions
@@ -65,7 +66,7 @@ const embedOriginsTrustedByUser = new Set();
65
  const isAlwaysTrustedForFetching = parsed => (
66
  // If we would trust loading an extension from here, we can trust loading resources too.
67
  isTrustedExtension(parsed.href) ||
68
- true ||
69
  // Any TurboWarp service such as trampoline
70
  parsed.origin === 'https://turbowarp.org' ||
71
  parsed.origin.endsWith('.turbowarp.org') ||
@@ -77,11 +78,16 @@ const isAlwaysTrustedForFetching = parsed => (
77
 
78
  // GitHub
79
  parsed.origin === 'https://raw.githubusercontent.com' ||
 
80
  parsed.origin === 'https://api.github.com' ||
81
 
82
- // GitLab
 
83
  parsed.origin === 'https://gitlab.com' ||
84
 
 
 
 
85
  // Itch
86
  parsed.origin.endsWith('.itch.io') ||
87
 
@@ -95,36 +101,69 @@ const isAlwaysTrustedForFetching = parsed => (
95
  parsed.origin === 'https://scratchdb.lefty.one'
96
  );
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  /**
99
  * @param {string} url Original URL string
 
100
  * @returns {URL|null} A URL object if it is valid and of a known protocol, otherwise null.
101
  */
102
- const parseURL = url => {
103
  let parsed;
104
  try {
105
  parsed = new URL(url);
106
  } catch (e) {
107
  return null;
108
  }
109
- const protocols = ['http:', 'https:', 'ws:', 'wss:', 'data:', 'blob:'];
110
  if (!protocols.includes(parsed.protocol)) {
111
  return null;
112
  }
113
  return parsed;
114
  };
115
 
116
- let allowedAudio = false;
117
- let allowedVideo = false;
118
- let allowedReadClipboard = false;
119
- let allowedNotify = false;
120
- let allowedGeolocation = false;
121
- const notAllowedToAskUnsandbox = Object.create(null);
122
- let loadingExtensionsRemember = false;
123
- let rememberedExtensionInfo = {
124
- unsandboxed: false,
125
- loaded: false
 
 
 
 
 
 
 
126
  };
127
 
 
 
 
 
 
 
128
  const SECURITY_MANAGER_METHODS = [
129
  'getSandboxMode',
130
  'canLoadExtensionFromProject',
@@ -137,7 +176,9 @@ const SECURITY_MANAGER_METHODS = [
137
  'canNotify',
138
  'canGeolocate',
139
  'canEmbed',
140
- 'canUnsandbox'
 
 
141
  ];
142
 
143
  class TWSecurityManagerComponent extends React.Component {
@@ -160,16 +201,23 @@ class TWSecurityManagerComponent extends React.Component {
160
  }
161
 
162
  projectWillChange() {
163
- loadingExtensionsRemember = false;
164
- rememberedExtensionInfo = {
165
- unsandboxed: false,
166
- loaded: false
 
 
 
 
 
 
167
  };
168
  }
169
  componentDidMount() {
170
- const securityManager = this.props.vm.extensionManager.securityManager;
 
171
  for (const method of SECURITY_MANAGER_METHODS) {
172
- securityManager[method] = this[method];
173
  }
174
  this.props.vm.runtime.on('RUNTIME_DISPOSED', this.projectWillChange);
175
  }
@@ -277,19 +325,19 @@ class TWSecurityManagerComponent extends React.Component {
277
  log.info(`Loading extension ${url} automatically`);
278
  return true;
279
  }
280
- if (loadingExtensionsRemember) {
281
- // TODO: find some way to identify these, custom extensions have too long of URLs
282
- if (!rememberedExtensionInfo.loaded) {
283
- console.warn('An extension was not loaded');
 
284
  return false;
285
  }
286
- if (rememberedExtensionInfo.unsandboxed) {
287
- console.log('An extension was loaded unsandboxed');
288
  manuallyTrustExtension(url);
289
  }
290
  return true;
291
  }
292
- const { showModal } = await this.acquireModalLock();
293
 
294
  // we allow all urls to be unsandboxed.
295
  // its very likely that people would load any file unsandboxed anyways, theres no safety in blocking it for urls only.
@@ -305,8 +353,8 @@ class TWSecurityManagerComponent extends React.Component {
305
  manuallyTrustExtension(url);
306
  }
307
  if (this.state.data.remember) {
308
- loadingExtensionsRemember = true;
309
- rememberedExtensionInfo = {
310
  unsandboxed: this.state.data.unsandboxed,
311
  loaded: allowed
312
  };
@@ -319,7 +367,7 @@ class TWSecurityManagerComponent extends React.Component {
319
  * @returns {Promise<boolean>} True if the resource is allowed to be fetched
320
  */
321
  async canFetch(url) {
322
- const parsed = parseURL(url);
323
  if (!parsed) {
324
  return false;
325
  }
@@ -327,16 +375,33 @@ class TWSecurityManagerComponent extends React.Component {
327
  return true;
328
  }
329
  const { showModal, releaseLock } = await this.acquireModalLock();
330
- if (fetchOriginsTrustedByUser.has(origin)) {
 
 
 
 
 
 
 
 
 
 
331
  releaseLock();
332
  return true;
333
  }
334
  const allowed = await showModal(SecurityModals.Fetch, {
335
- url
 
 
336
  });
337
  if (allowed) {
338
  fetchOriginsTrustedByUser.add(origin);
339
  }
 
 
 
 
 
340
  return allowed;
341
  }
342
 
@@ -345,7 +410,7 @@ class TWSecurityManagerComponent extends React.Component {
345
  * @returns {Promise<boolean>} True if the website can be opened
346
  */
347
  async canOpenWindow(url) {
348
- const parsed = parseURL(url);
349
  if (!parsed) {
350
  return false;
351
  }
@@ -360,7 +425,7 @@ class TWSecurityManagerComponent extends React.Component {
360
  * @returns {Promise<boolean>} True if the website can be redirected to
361
  */
362
  async canRedirect(url) {
363
- const parsed = parseURL(url);
364
  if (!parsed) {
365
  return false;
366
  }
@@ -425,16 +490,26 @@ class TWSecurityManagerComponent extends React.Component {
425
  return allowedGeolocation;
426
  }
427
 
 
 
 
 
 
 
 
 
 
 
428
 
429
  /**
430
- * @returns {Promise<boolean>} True if geolocation is allowed.
431
  */
432
  async canUnsandbox(name) {
433
- if (notAllowedToAskUnsandbox[name]) return false;
434
  const { showModal } = await this.acquireModalLock();
435
  const allowedUnsandbox = await showModal(SecurityModals.Unsandbox, { name: name || "" });
436
  if (!allowedUnsandbox) {
437
- notAllowedToAskUnsandbox[name] = true;
438
  }
439
  return allowedUnsandbox;
440
  }
@@ -444,20 +519,77 @@ class TWSecurityManagerComponent extends React.Component {
444
  * @returns {Promise<boolean>} True if embed is allowed.
445
  */
446
  async canEmbed(url) {
447
- const parsed = parseURL(url);
448
  if (!parsed) {
449
  return false;
450
  }
451
  const origin = (parsed.protocol === 'http:' || parsed.protocol === 'https:') ? parsed.origin : null;
452
  const { showModal, releaseLock } = await this.acquireModalLock();
 
 
 
 
 
 
 
 
 
453
  if (origin && embedOriginsTrustedByUser.has(origin)) {
454
  releaseLock();
455
  return true;
456
  }
457
- const allowed = await showModal(SecurityModals.Embed, { url });
 
 
 
 
458
  if (origin && allowed) {
459
  embedOriginsTrustedByUser.add(origin);
460
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461
  return allowed;
462
  }
463
 
@@ -488,7 +620,12 @@ TWSecurityManagerComponent.propTypes = {
488
  }, {})
489
  ).isRequired
490
  }).isRequired
491
- }).isRequired
 
 
 
 
 
492
  };
493
 
494
  const mapStateToProps = state => ({
@@ -507,4 +644,4 @@ export {
507
  manuallyTrustExtension,
508
  isTrustedExtension,
509
  isTrustedExtensionOrigin
510
- };
 
5
  import bindAll from 'lodash.bindall';
6
  import SecurityManagerModal from '../components/tw-security-manager-modal/security-manager-modal.jsx';
7
  import SecurityModals from '../lib/tw-security-manager-constants';
8
+ import { isDefinitelyExecutable } from '../lib/pm-security-manager-download-util.js';
9
 
10
  /**
11
  * Set of extension URLs that the user has manually trusted to load unsandboxed.
 
32
  url.startsWith('https://pen-group.github.io/') || // Pen-Group / ObviousAlexC
33
 
34
  // For development.
35
+ url.startsWith('http://localhost:8000') ||
36
  url.startsWith('http://localhost:6000') || // Launcher Home
37
  url.startsWith('http://localhost:6001') || // Launcher Extensions
38
  url.startsWith('http://localhost:5173') || // Local Home or Extensions
 
66
  const isAlwaysTrustedForFetching = parsed => (
67
  // If we would trust loading an extension from here, we can trust loading resources too.
68
  isTrustedExtension(parsed.href) ||
69
+
70
  // Any TurboWarp service such as trampoline
71
  parsed.origin === 'https://turbowarp.org' ||
72
  parsed.origin.endsWith('.turbowarp.org') ||
 
78
 
79
  // GitHub
80
  parsed.origin === 'https://raw.githubusercontent.com' ||
81
+ parsed.origin === 'https://gist.githubusercontent.com' ||
82
  parsed.origin === 'https://api.github.com' ||
83
 
84
+ // GitLab API
85
+ // GitLab Pages allows redirects, so not included here.
86
  parsed.origin === 'https://gitlab.com' ||
87
 
88
+ // Sourcehut Pages
89
+ parsed.origin.endsWith('.srht.site') ||
90
+
91
  // Itch
92
  parsed.origin.endsWith('.itch.io') ||
93
 
 
101
  parsed.origin === 'https://scratchdb.lefty.one'
102
  );
103
 
104
+ const FETCHABLE_PROTOCOLS = [
105
+ 'http:',
106
+ 'https:',
107
+ 'data:',
108
+ 'blob:',
109
+ 'ws:',
110
+ 'wss:'
111
+ ];
112
+
113
+ const VISITABLE_PROTOCOLS = [
114
+ // The important one we want to exclude is javascript:
115
+ 'http:',
116
+ 'https:',
117
+ 'data:',
118
+ 'blob:',
119
+ 'mailto:',
120
+ 'steam:',
121
+ 'calculator:'
122
+ ];
123
+
124
  /**
125
  * @param {string} url Original URL string
126
+ * @param {string[]} protocols List of allowed protocols
127
  * @returns {URL|null} A URL object if it is valid and of a known protocol, otherwise null.
128
  */
129
+ const parseURL = (url, protocols) => {
130
  let parsed;
131
  try {
132
  parsed = new URL(url);
133
  } catch (e) {
134
  return null;
135
  }
 
136
  if (!protocols.includes(parsed.protocol)) {
137
  return null;
138
  }
139
  return parsed;
140
  };
141
 
142
+ let allowedAudio = true;
143
+ let allowedVideo = true;
144
+ let allowedReadClipboard = true;
145
+ let allowedNotify = true;
146
+ let allowedGeolocation = true;
147
+ let allowedScreenshotCamera = true;
148
+
149
+ let rememberFetchSitesDecision = true;
150
+ let rememberFetchSitesAllAllowed = true;
151
+ let rememberEmbedSitesDecision = true;
152
+ let rememberEmbedSitesAllAllowed = true;
153
+ let rememberDownloadDecision = true;
154
+ let rememberDownloadAllAllowed = true;
155
+ let rememberLoadingExtensions = true;
156
+ let rememberLoadingExtensionsInfo = {
157
+ unsandboxed: true,
158
+ loaded: true
159
  };
160
 
161
+ /**
162
+ * A list of developer defined names that are not allowed to ask for unsandboxing.
163
+ * @type {Set<string>}
164
+ */
165
+ const notAllowedToAskUnsandbox = new Set();
166
+
167
  const SECURITY_MANAGER_METHODS = [
168
  'getSandboxMode',
169
  'canLoadExtensionFromProject',
 
176
  'canNotify',
177
  'canGeolocate',
178
  'canEmbed',
179
+ 'canUnsandbox',
180
+ 'canScreenshotCamera',
181
+ 'canDownload'
182
  ];
183
 
184
  class TWSecurityManagerComponent extends React.Component {
 
201
  }
202
 
203
  projectWillChange() {
204
+ rememberFetchSitesDecision = true;
205
+ rememberFetchSitesAllAllowed = true;
206
+ rememberEmbedSitesDecision = true;
207
+ rememberEmbedSitesAllAllowed = true;
208
+ rememberDownloadDecision = true;
209
+ rememberDownloadAllAllowed = true;
210
+ rememberLoadingExtensions = true;
211
+ rememberLoadingExtensionsInfo = {
212
+ unsandboxed: true,
213
+ loaded: true
214
  };
215
  }
216
  componentDidMount() {
217
+ const vmSecurityManager = this.props.vm.extensionManager.securityManager;
218
+ const propsSecurityManager = this.props.securityManager;
219
  for (const method of SECURITY_MANAGER_METHODS) {
220
+ vmSecurityManager[method] = propsSecurityManager[method] || this[method];
221
  }
222
  this.props.vm.runtime.on('RUNTIME_DISPOSED', this.projectWillChange);
223
  }
 
325
  log.info(`Loading extension ${url} automatically`);
326
  return true;
327
  }
328
+ const { showModal, releaseLock } = await this.acquireModalLock();
329
+ if (rememberLoadingExtensions) {
330
+ releaseLock();
331
+ if (!rememberLoadingExtensionsInfo.loaded) {
332
+ log.info('An unseen extension was automatically not loaded');
333
  return false;
334
  }
335
+ if (rememberLoadingExtensionsInfo.unsandboxed) {
336
+ log.warn('An unseen extension was automatically loaded unsandboxed');
337
  manuallyTrustExtension(url);
338
  }
339
  return true;
340
  }
 
341
 
342
  // we allow all urls to be unsandboxed.
343
  // its very likely that people would load any file unsandboxed anyways, theres no safety in blocking it for urls only.
 
353
  manuallyTrustExtension(url);
354
  }
355
  if (this.state.data.remember) {
356
+ rememberLoadingExtensions = true;
357
+ rememberLoadingExtensionsInfo = {
358
  unsandboxed: this.state.data.unsandboxed,
359
  loaded: allowed
360
  };
 
367
  * @returns {Promise<boolean>} True if the resource is allowed to be fetched
368
  */
369
  async canFetch(url) {
370
+ const parsed = parseURL(url, FETCHABLE_PROTOCOLS);
371
  if (!parsed) {
372
  return false;
373
  }
 
375
  return true;
376
  }
377
  const { showModal, releaseLock } = await this.acquireModalLock();
378
+ const origin = (parsed.protocol === 'http:' || parsed.protocol === 'https:') ? parsed.origin : null;
379
+ if (rememberFetchSitesDecision) {
380
+ releaseLock();
381
+ if (rememberFetchSitesAllAllowed) {
382
+ log.warn(url, "was automatically fetched without prompt");
383
+ } else {
384
+ log.info(url, "was automatically denied without prompt");
385
+ }
386
+ return rememberFetchSitesAllAllowed;
387
+ }
388
+ if (origin && fetchOriginsTrustedByUser.has(origin)) {
389
  releaseLock();
390
  return true;
391
  }
392
  const allowed = await showModal(SecurityModals.Fetch, {
393
+ url,
394
+ remember: false,
395
+ onChangeRemember: this.handleChangeRemember.bind(this),
396
  });
397
  if (allowed) {
398
  fetchOriginsTrustedByUser.add(origin);
399
  }
400
+ if (this.state.data.remember) {
401
+ rememberFetchSitesDecision = true;
402
+ rememberFetchSitesAllAllowed = allowed;
403
+ log.info("Remembering to allow all sites?", rememberFetchSitesAllAllowed);
404
+ }
405
  return allowed;
406
  }
407
 
 
410
  * @returns {Promise<boolean>} True if the website can be opened
411
  */
412
  async canOpenWindow(url) {
413
+ const parsed = parseURL(url, VISITABLE_PROTOCOLS);
414
  if (!parsed) {
415
  return false;
416
  }
 
425
  * @returns {Promise<boolean>} True if the website can be redirected to
426
  */
427
  async canRedirect(url) {
428
+ const parsed = parseURL(url, VISITABLE_PROTOCOLS);
429
  if (!parsed) {
430
  return false;
431
  }
 
490
  return allowedGeolocation;
491
  }
492
 
493
+ /**
494
+ * @returns {Promise<boolean>} True if screenshotting the camera is allowed.
495
+ */
496
+ async canScreenshotCamera() {
497
+ if (!allowedScreenshotCamera) {
498
+ const { showModal } = await this.acquireModalLock();
499
+ allowedScreenshotCamera = await showModal(SecurityModals.ScreenshotCamera);
500
+ }
501
+ return allowedScreenshotCamera;
502
+ }
503
 
504
  /**
505
+ * @returns {Promise<boolean>} True if unsandboxing the provided extension name is allowed.
506
  */
507
  async canUnsandbox(name) {
508
+ if (notAllowedToAskUnsandbox.has(name)) return false;
509
  const { showModal } = await this.acquireModalLock();
510
  const allowedUnsandbox = await showModal(SecurityModals.Unsandbox, { name: name || "" });
511
  if (!allowedUnsandbox) {
512
+ notAllowedToAskUnsandbox.add(name);
513
  }
514
  return allowedUnsandbox;
515
  }
 
519
  * @returns {Promise<boolean>} True if embed is allowed.
520
  */
521
  async canEmbed(url) {
522
+ const parsed = parseURL(url, FETCHABLE_PROTOCOLS);
523
  if (!parsed) {
524
  return false;
525
  }
526
  const origin = (parsed.protocol === 'http:' || parsed.protocol === 'https:') ? parsed.origin : null;
527
  const { showModal, releaseLock } = await this.acquireModalLock();
528
+ if (rememberEmbedSitesDecision) {
529
+ releaseLock();
530
+ if (rememberEmbedSitesAllAllowed) {
531
+ log.warn(url, "was automatically embedded without prompt");
532
+ } else {
533
+ log.info(url, "was automatically embed denied without prompt");
534
+ }
535
+ return rememberEmbedSitesAllAllowed;
536
+ }
537
  if (origin && embedOriginsTrustedByUser.has(origin)) {
538
  releaseLock();
539
  return true;
540
  }
541
+ const allowed = await showModal(SecurityModals.Embed, {
542
+ url,
543
+ remember: false,
544
+ onChangeRemember: this.handleChangeRemember.bind(this),
545
+ });
546
  if (origin && allowed) {
547
  embedOriginsTrustedByUser.add(origin);
548
  }
549
+ if (this.state.data.remember) {
550
+ rememberEmbedSitesDecision = true;
551
+ rememberEmbedSitesAllAllowed = allowed;
552
+ log.info("Remembering to allow embedding all sites?", rememberEmbedSitesAllAllowed);
553
+ }
554
+ return allowed;
555
+ }
556
+
557
+ /**
558
+ * @param {string} url URL to download
559
+ * @param {string} name Name to download as
560
+ * @returns {Promise<boolean>} True if allowed
561
+ */
562
+ async canDownload(url, name) {
563
+ const parsed = parseURL(url, FETCHABLE_PROTOCOLS);
564
+ if (!parsed) {
565
+ return false;
566
+ }
567
+ // pm: We only prompt the user for known executables.
568
+ // See src/lib/pm-security-manager-download-util.js for details.
569
+ if (!isDefinitelyExecutable(name)) {
570
+ return true;
571
+ }
572
+ const { showModal, releaseLock } = await this.acquireModalLock();
573
+ if (rememberDownloadDecision) {
574
+ releaseLock();
575
+ if (rememberDownloadAllAllowed) {
576
+ log.warn(url, "was automatically downloaded without prompt");
577
+ } else {
578
+ log.info(url, "was automatically download denied without prompt");
579
+ }
580
+ return rememberDownloadAllAllowed;
581
+ }
582
+ const allowed = await showModal(SecurityModals.Download, {
583
+ url,
584
+ name,
585
+ remember: false,
586
+ onChangeRemember: this.handleChangeRemember.bind(this),
587
+ });
588
+ if (this.state.data.remember) {
589
+ rememberDownloadDecision = true;
590
+ rememberDownloadAllAllowed = allowed;
591
+ log.info("Remembering to allow downloading all files?", rememberDownloadAllAllowed);
592
+ }
593
  return allowed;
594
  }
595
 
 
620
  }, {})
621
  ).isRequired
622
  }).isRequired
623
+ }).isRequired,
624
+ securityManager: PropTypes.shape(Object.fromEntries(SECURITY_MANAGER_METHODS.map(i => [i, PropTypes.func])))
625
+ };
626
+
627
+ TWSecurityManagerComponent.defaultProps = {
628
+ securityManager: {}
629
  };
630
 
631
  const mapStateToProps = state => ({
 
644
  manuallyTrustExtension,
645
  isTrustedExtension,
646
  isTrustedExtensionOrigin
647
+ };