guest-window-proxy.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. /**
  2. * Manage guest windows when using the default BrowserWindowProxy version of the
  3. * renderer's window.open (i.e. nativeWindowOpen off). This module mostly
  4. * consists of marshaling IPC requests from the BrowserWindowProxy to the
  5. * WebContents.
  6. */
  7. import { webContents, BrowserWindow } from 'electron/main';
  8. import type { WebContents } from 'electron/main';
  9. import { ipcMainInternal } from '@electron/internal/browser/ipc-main-internal';
  10. import * as ipcMainUtils from '@electron/internal/browser/ipc-main-internal-utils';
  11. import { openGuestWindow } from '@electron/internal/browser/guest-window-manager';
  12. import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages';
  13. const { isSameOrigin } = process._linkedBinding('electron_common_v8_util');
  14. const getGuestWindow = function (guestContents: WebContents) {
  15. let guestWindow = BrowserWindow.fromWebContents(guestContents);
  16. if (guestWindow == null) {
  17. const hostContents = guestContents.hostWebContents;
  18. if (hostContents != null) {
  19. guestWindow = BrowserWindow.fromWebContents(hostContents);
  20. }
  21. }
  22. if (!guestWindow) {
  23. throw new Error('getGuestWindow failed');
  24. }
  25. return guestWindow;
  26. };
  27. const isChildWindow = function (sender: WebContents, target: WebContents) {
  28. return (target as any).getLastWebPreferences().openerId === sender.id;
  29. };
  30. const isRelatedWindow = function (sender: WebContents, target: WebContents) {
  31. return isChildWindow(sender, target) || isChildWindow(target, sender);
  32. };
  33. const isScriptableWindow = function (sender: WebContents, target: WebContents) {
  34. return (
  35. isRelatedWindow(sender, target) &&
  36. isSameOrigin(sender.getURL(), target.getURL())
  37. );
  38. };
  39. const isNodeIntegrationEnabled = function (sender: WebContents) {
  40. return (sender as any).getLastWebPreferences().nodeIntegration === true;
  41. };
  42. // Checks whether |sender| can access the |target|:
  43. const canAccessWindow = function (sender: WebContents, target: WebContents) {
  44. return (
  45. isChildWindow(sender, target) ||
  46. isScriptableWindow(sender, target) ||
  47. isNodeIntegrationEnabled(sender)
  48. );
  49. };
  50. // Routed window.open messages with raw options
  51. ipcMainInternal.on(
  52. IPC_MESSAGES.GUEST_WINDOW_MANAGER_WINDOW_OPEN,
  53. (
  54. event,
  55. url: string,
  56. frameName: string,
  57. features: string
  58. ) => {
  59. // This should only be allowed for senders that have nativeWindowOpen: false
  60. const lastWebPreferences = (event.sender as any).getLastWebPreferences();
  61. if (lastWebPreferences.nativeWindowOpen || lastWebPreferences.sandbox) {
  62. (event as any).returnValue = null;
  63. throw new Error(
  64. 'GUEST_WINDOW_MANAGER_WINDOW_OPEN denied: expected native window.open'
  65. );
  66. }
  67. const browserWindowOptions = (event.sender as any)._callWindowOpenHandler(event, url, frameName, features);
  68. if (event.defaultPrevented) {
  69. event.returnValue = null;
  70. return;
  71. }
  72. const guest = openGuestWindow({
  73. event,
  74. embedder: event.sender,
  75. referrer: { url: '', policy: 'default' },
  76. disposition: 'new-window',
  77. overrideBrowserWindowOptions: browserWindowOptions,
  78. windowOpenArgs: {
  79. url: url || 'about:blank',
  80. frameName: frameName || '',
  81. features: features || ''
  82. }
  83. });
  84. (event as any).returnValue = guest ? guest.webContents.id : null;
  85. }
  86. );
  87. type IpcHandler<T, Event> = (event: Event, guestContents: Electron.WebContents, ...args: any[]) => T;
  88. const makeSafeHandler = function<T, Event> (handler: IpcHandler<T, Event>) {
  89. return (event: Event, guestId: number, ...args: any[]) => {
  90. // Access webContents via electron to prevent circular require.
  91. const guestContents = webContents.fromId(guestId);
  92. if (!guestContents) {
  93. throw new Error(`Invalid guestId: ${guestId}`);
  94. }
  95. return handler(event, guestContents as Electron.WebContents, ...args);
  96. };
  97. };
  98. const handleMessage = function (channel: string, handler: IpcHandler<any, Electron.IpcMainInvokeEvent>) {
  99. ipcMainInternal.handle(channel, makeSafeHandler(handler));
  100. };
  101. const handleMessageSync = function (channel: string, handler: IpcHandler<any, ElectronInternal.IpcMainInternalEvent>) {
  102. ipcMainUtils.handleSync(channel, makeSafeHandler(handler));
  103. };
  104. type ContentsCheck = (contents: WebContents, guestContents: WebContents) => boolean;
  105. const securityCheck = function (contents: WebContents, guestContents: WebContents, check: ContentsCheck) {
  106. if (!check(contents, guestContents)) {
  107. console.error(
  108. `Blocked ${contents.getURL()} from accessing guestId: ${guestContents.id}`
  109. );
  110. throw new Error(`Access denied to guestId: ${guestContents.id}`);
  111. }
  112. };
  113. const windowMethods = new Set(['destroy', 'focus', 'blur']);
  114. handleMessage(
  115. IPC_MESSAGES.GUEST_WINDOW_MANAGER_WINDOW_METHOD,
  116. (event, guestContents, method, ...args) => {
  117. securityCheck(event.sender, guestContents, canAccessWindow);
  118. if (!windowMethods.has(method)) {
  119. console.error(
  120. `Blocked ${event.sender.getURL()} from calling method: ${method}`
  121. );
  122. throw new Error(`Invalid method: ${method}`);
  123. }
  124. return (getGuestWindow(guestContents) as any)[method](...args);
  125. }
  126. );
  127. handleMessage(
  128. IPC_MESSAGES.GUEST_WINDOW_MANAGER_WINDOW_POSTMESSAGE,
  129. (event, guestContents, message, targetOrigin, sourceOrigin) => {
  130. if (targetOrigin == null) {
  131. targetOrigin = '*';
  132. }
  133. // The W3C does not seem to have word on how postMessage should work when the
  134. // origins do not match, so we do not do |canAccessWindow| check here since
  135. // postMessage across origins is useful and not harmful.
  136. securityCheck(event.sender, guestContents, isRelatedWindow);
  137. if (
  138. targetOrigin === '*' ||
  139. isSameOrigin(guestContents.getURL(), targetOrigin)
  140. ) {
  141. const sourceId = event.sender.id;
  142. guestContents._sendInternal(
  143. IPC_MESSAGES.GUEST_WINDOW_POSTMESSAGE,
  144. sourceId,
  145. message,
  146. sourceOrigin
  147. );
  148. }
  149. }
  150. );
  151. const webContentsMethodsAsync = new Set([
  152. 'loadURL',
  153. 'executeJavaScript',
  154. 'print'
  155. ]);
  156. handleMessage(
  157. IPC_MESSAGES.GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD,
  158. (event, guestContents, method, ...args) => {
  159. securityCheck(event.sender, guestContents, canAccessWindow);
  160. if (!webContentsMethodsAsync.has(method)) {
  161. console.error(
  162. `Blocked ${event.sender.getURL()} from calling method: ${method}`
  163. );
  164. throw new Error(`Invalid method: ${method}`);
  165. }
  166. return (guestContents as any)[method](...args);
  167. }
  168. );
  169. const webContentsMethodsSync = new Set(['getURL']);
  170. handleMessageSync(
  171. IPC_MESSAGES.GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD,
  172. (event, guestContents, method, ...args) => {
  173. securityCheck(event.sender, guestContents, canAccessWindow);
  174. if (!webContentsMethodsSync.has(method)) {
  175. console.error(
  176. `Blocked ${event.sender.getURL()} from calling method: ${method}`
  177. );
  178. throw new Error(`Invalid method: ${method}`);
  179. }
  180. return (guestContents as any)[method](...args);
  181. }
  182. );