aboutsummaryrefslogtreecommitdiffstats
path: root/packages/main/src/infrastructure/electron/impl/hardenSession.ts
blob: 71d8148dfda1880fbcb4a2868d527b4d24934e66 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/*
 * Copyright (C)  2022 Kristóf Marussy <kristof@marussy.com>
 *
 * This file is part of Sophie.
 *
 * Sophie is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, version 3.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: AGPL-3.0-only
 */

import { URL } from 'node:url';

import type { Session } from 'electron';

import { getLogger } from '../../../utils/log';
import type Resources from '../../resources/Resources';

import { DEVMODE_ALLOWED_URL_PREFIXES } from './devTools';

const log = getLogger('hardenSession');

/**
 * Hardens a session to prevent loading resources outside the renderer resources and
 * to reject all permission requests.
 *
 * In dev mode, installation of extensions and opening the devtools will be allowed.
 *
 * @param resources The resource handle associated with the paths and URL of the application.
 * @param devMode Whether the application is in development mode.
 * @param session The session to harden.
 */
export default function hardenSession(
  resources: Resources,
  devMode: boolean,
  session: Session,
): void {
  session.setPermissionRequestHandler((_webContents, _permission, callback) => {
    callback(false);
  });

  const rendererBaseURL = resources.getRendererURL('/');
  log.debug('Renderer base URL:', rendererBaseURL);

  const webSocketBaseURL = rendererBaseURL.replace(/^http(s)?:/, 'ws$1:');
  log.debug('WebSocket base URL:', webSocketBaseURL);

  function shouldCancelRequest(url: string, method: string): boolean {
    if (method !== 'GET') {
      return true;
    }
    let normalizedURL: string;
    try {
      normalizedURL = new URL(url).toString();
    } catch {
      return true;
    }
    if (
      devMode &&
      DEVMODE_ALLOWED_URL_PREFIXES.some((prefix) =>
        normalizedURL.startsWith(prefix),
      )
    ) {
      return false;
    }
    const isHttp = normalizedURL.startsWith(rendererBaseURL);
    const isWs = normalizedURL.startsWith(webSocketBaseURL);
    return !isHttp && !isWs;
  }

  session.webRequest.onBeforeRequest(({ url, method }, callback) => {
    const cancel = shouldCancelRequest(url, method);
    if (cancel) {
      log.error('Prevented loading', method, url);
    }
    callback({ cancel });
  });
}