fix: prevent node mode to be used as script runner by other apps (#40579)
This commit is contained in:
parent
9aa73abe78
commit
cb0da6ff34
8 changed files with 301 additions and 88 deletions
|
@ -199,6 +199,8 @@ filenames = {
|
||||||
"shell/common/language_util_mac.mm",
|
"shell/common/language_util_mac.mm",
|
||||||
"shell/common/mac/main_application_bundle.h",
|
"shell/common/mac/main_application_bundle.h",
|
||||||
"shell/common/mac/main_application_bundle.mm",
|
"shell/common/mac/main_application_bundle.mm",
|
||||||
|
"shell/common/mac/codesign_util.cc",
|
||||||
|
"shell/common/mac/codesign_util.h",
|
||||||
"shell/common/node_bindings_mac.cc",
|
"shell/common/node_bindings_mac.cc",
|
||||||
"shell/common/node_bindings_mac.h",
|
"shell/common/node_bindings_mac.h",
|
||||||
"shell/common/platform_util_mac.mm",
|
"shell/common/platform_util_mac.mm",
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
#include "base/base_switches.h"
|
#include "base/base_switches.h"
|
||||||
#include "base/command_line.h"
|
#include "base/command_line.h"
|
||||||
#include "base/containers/fixed_flat_set.h"
|
#include "base/containers/fixed_flat_set.h"
|
||||||
|
#include "base/environment.h"
|
||||||
#include "base/feature_list.h"
|
#include "base/feature_list.h"
|
||||||
#include "base/strings/string_util.h"
|
#include "base/strings/string_util.h"
|
||||||
#include "base/strings/utf_string_conversions.h"
|
#include "base/strings/utf_string_conversions.h"
|
||||||
|
@ -20,6 +21,7 @@
|
||||||
#include "base/task/thread_pool/thread_pool_instance.h"
|
#include "base/task/thread_pool/thread_pool_instance.h"
|
||||||
#include "content/public/common/content_switches.h"
|
#include "content/public/common/content_switches.h"
|
||||||
#include "electron/electron_version.h"
|
#include "electron/electron_version.h"
|
||||||
|
#include "electron/fuses.h"
|
||||||
#include "gin/array_buffer.h"
|
#include "gin/array_buffer.h"
|
||||||
#include "gin/public/isolate_holder.h"
|
#include "gin/public/isolate_holder.h"
|
||||||
#include "gin/v8_initializer.h"
|
#include "gin/v8_initializer.h"
|
||||||
|
@ -35,13 +37,16 @@
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if BUILDFLAG(IS_LINUX)
|
#if BUILDFLAG(IS_LINUX)
|
||||||
#include "base/environment.h"
|
|
||||||
#include "base/posix/global_descriptors.h"
|
#include "base/posix/global_descriptors.h"
|
||||||
#include "base/strings/string_number_conversions.h"
|
#include "base/strings/string_number_conversions.h"
|
||||||
#include "components/crash/core/app/crash_switches.h" // nogncheck
|
#include "components/crash/core/app/crash_switches.h" // nogncheck
|
||||||
#include "content/public/common/content_descriptors.h"
|
#include "content/public/common/content_descriptors.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if BUILDFLAG(IS_MAC)
|
||||||
|
#include "shell/common/mac/codesign_util.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#if !IS_MAS_BUILD()
|
#if !IS_MAS_BUILD()
|
||||||
#include "components/crash/core/app/crashpad.h" // nogncheck
|
#include "components/crash/core/app/crashpad.h" // nogncheck
|
||||||
#include "shell/app/electron_crash_reporter_client.h"
|
#include "shell/app/electron_crash_reporter_client.h"
|
||||||
|
@ -100,12 +105,36 @@ int NodeMain(int argc, char* argv[]) {
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto os_env = base::Environment::Create();
|
||||||
|
bool node_options_enabled = electron::fuses::IsNodeOptionsEnabled();
|
||||||
|
#if BUILDFLAG(IS_MAC)
|
||||||
|
if (node_options_enabled && os_env->HasVar("NODE_OPTIONS")) {
|
||||||
|
// On macOS, it is forbidden to run sandboxed app with custom arguments
|
||||||
|
// from another app, i.e. args are discarded in following call:
|
||||||
|
// exec("Sandboxed.app", ["--custom-args-will-be-discarded"])
|
||||||
|
// However it is possible to bypass the restriction by abusing the node mode
|
||||||
|
// of Electron apps:
|
||||||
|
// exec("Electron.app", {env: {ELECTRON_RUN_AS_NODE: "1",
|
||||||
|
// NODE_OPTIONS: "--require 'bad.js'"}})
|
||||||
|
// To prevent Electron apps from being used to work around macOS security
|
||||||
|
// restrictions, when NODE_OPTIONS is passed it will be checked whether
|
||||||
|
// this process is invoked by its own app.
|
||||||
|
if (!ProcessBelongToCurrentApp(getppid())) {
|
||||||
|
LOG(ERROR) << "NODE_OPTIONS is disabled because this process is invoked "
|
||||||
|
"by other apps.";
|
||||||
|
node_options_enabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif // BUILDFLAG(IS_MAC)
|
||||||
|
if (!node_options_enabled) {
|
||||||
|
os_env->UnSetVar("NODE_OPTIONS");
|
||||||
|
}
|
||||||
|
|
||||||
#if BUILDFLAG(IS_WIN)
|
#if BUILDFLAG(IS_WIN)
|
||||||
v8_crashpad_support::SetUp();
|
v8_crashpad_support::SetUp();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if BUILDFLAG(IS_LINUX)
|
#if BUILDFLAG(IS_LINUX)
|
||||||
auto os_env = base::Environment::Create();
|
|
||||||
std::string fd_string, pid_string;
|
std::string fd_string, pid_string;
|
||||||
if (os_env->GetVar("CRASHDUMP_SIGNAL_FD", &fd_string) &&
|
if (os_env->GetVar("CRASHDUMP_SIGNAL_FD", &fd_string) &&
|
||||||
os_env->GetVar("CRASHPAD_HANDLER_PID", &pid_string)) {
|
os_env->GetVar("CRASHPAD_HANDLER_PID", &pid_string)) {
|
||||||
|
|
55
shell/common/mac/codesign_util.cc
Normal file
55
shell/common/mac/codesign_util.cc
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
// Copyright 2023 Microsoft, Inc.
|
||||||
|
// Copyright 2013 The Chromium Authors
|
||||||
|
// Use of this source code is governed by the MIT license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
#include "shell/common/mac/codesign_util.h"
|
||||||
|
|
||||||
|
#include "base/apple/osstatus_logging.h"
|
||||||
|
#include "base/apple/scoped_cftyperef.h"
|
||||||
|
|
||||||
|
#include <CoreFoundation/CoreFoundation.h>
|
||||||
|
#include <Security/Security.h>
|
||||||
|
|
||||||
|
namespace electron {
|
||||||
|
|
||||||
|
bool ProcessBelongToCurrentApp(pid_t pid) {
|
||||||
|
// Get and check the code signature of current app.
|
||||||
|
base::apple::ScopedCFTypeRef<SecCodeRef> self_code;
|
||||||
|
OSStatus status =
|
||||||
|
SecCodeCopySelf(kSecCSDefaultFlags, self_code.InitializeInto());
|
||||||
|
if (status != errSecSuccess) {
|
||||||
|
OSSTATUS_LOG(ERROR, status) << "SecCodeCopyGuestWithAttributes";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Get the code signature of process.
|
||||||
|
base::apple::ScopedCFTypeRef<CFNumberRef> process_cf(
|
||||||
|
CFNumberCreate(nullptr, kCFNumberIntType, &pid));
|
||||||
|
const void* attribute_keys[] = {kSecGuestAttributePid};
|
||||||
|
const void* attribute_values[] = {process_cf.get()};
|
||||||
|
base::apple::ScopedCFTypeRef<CFDictionaryRef> attributes(CFDictionaryCreate(
|
||||||
|
nullptr, attribute_keys, attribute_values, std::size(attribute_keys),
|
||||||
|
&kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks));
|
||||||
|
base::apple::ScopedCFTypeRef<SecCodeRef> process_code;
|
||||||
|
status = SecCodeCopyGuestWithAttributes(nullptr, attributes.get(),
|
||||||
|
kSecCSDefaultFlags,
|
||||||
|
process_code.InitializeInto());
|
||||||
|
if (status != errSecSuccess) {
|
||||||
|
OSSTATUS_LOG(ERROR, status) << "SecCodeCopyGuestWithAttributes";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Get the requirement of current app's code signature.
|
||||||
|
base::apple::ScopedCFTypeRef<SecRequirementRef> self_requirement;
|
||||||
|
status = SecCodeCopyDesignatedRequirement(self_code.get(), kSecCSDefaultFlags,
|
||||||
|
self_requirement.InitializeInto());
|
||||||
|
if (status != errSecSuccess) {
|
||||||
|
OSSTATUS_LOG(ERROR, status) << "SecCodeCopyDesignatedRequirement";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Check whether the process meets the signature requirement of current app.
|
||||||
|
status = SecCodeCheckValidity(process_code.get(), kSecCSDefaultFlags,
|
||||||
|
self_requirement.get());
|
||||||
|
return status == errSecSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace electron
|
19
shell/common/mac/codesign_util.h
Normal file
19
shell/common/mac/codesign_util.h
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
// Copyright 2023 Microsoft, Inc.
|
||||||
|
// Copyright 2013 The Chromium Authors
|
||||||
|
// Use of this source code is governed by the MIT license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
#ifndef SHELL_COMMON_MAC_CODESIGN_UTIL_H_
|
||||||
|
#define SHELL_COMMON_MAC_CODESIGN_UTIL_H_
|
||||||
|
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
namespace electron {
|
||||||
|
|
||||||
|
// Given a pid, check if the process belongs to current app by comparing its
|
||||||
|
// code signature with current app.
|
||||||
|
bool ProcessBelongToCurrentApp(pid_t pid);
|
||||||
|
|
||||||
|
} // namespace electron
|
||||||
|
|
||||||
|
#endif // SHELL_COMMON_MAC_CODESIGN_UTIL_H_
|
|
@ -3,36 +3,26 @@ import * as cp from 'node:child_process';
|
||||||
import * as http from 'node:http';
|
import * as http from 'node:http';
|
||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
import * as fs from 'fs-extra';
|
import * as fs from 'fs-extra';
|
||||||
import * as os from 'node:os';
|
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
import * as psList from 'ps-list';
|
import * as psList from 'ps-list';
|
||||||
import { AddressInfo } from 'node:net';
|
import { AddressInfo } from 'node:net';
|
||||||
import { ifdescribe, ifit } from './lib/spec-helpers';
|
import { ifdescribe, ifit } from './lib/spec-helpers';
|
||||||
|
import { copyApp, getCodesignIdentity, shouldRunCodesignTests, signApp, spawn, withTempDirectory } from './lib/codesign-helpers';
|
||||||
import * as uuid from 'uuid';
|
import * as uuid from 'uuid';
|
||||||
import { autoUpdater, systemPreferences } from 'electron';
|
import { autoUpdater, systemPreferences } from 'electron';
|
||||||
|
|
||||||
const features = process._linkedBinding('electron_common_features');
|
|
||||||
|
|
||||||
const fixturesPath = path.resolve(__dirname, 'fixtures');
|
|
||||||
|
|
||||||
// We can only test the auto updater on darwin non-component builds
|
// We can only test the auto updater on darwin non-component builds
|
||||||
ifdescribe(process.platform === 'darwin' && !(process.env.CI && process.arch === 'arm64') && !process.mas && !features.isComponentBuild())('autoUpdater behavior', function () {
|
ifdescribe(shouldRunCodesignTests)('autoUpdater behavior', function () {
|
||||||
this.timeout(120000);
|
this.timeout(120000);
|
||||||
|
|
||||||
let identity = '';
|
let identity = '';
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
const result = cp.spawnSync(path.resolve(__dirname, '../script/codesign/get-trusted-identity.sh'));
|
const result = getCodesignIdentity();
|
||||||
if (result.status !== 0 || result.stdout.toString().trim().length === 0) {
|
if (result === null) {
|
||||||
// Per https://circleci.com/docs/2.0/env-vars:
|
|
||||||
// CIRCLE_PR_NUMBER is only present on forked PRs
|
|
||||||
if (process.env.CI && !process.env.CIRCLE_PR_NUMBER) {
|
|
||||||
throw new Error('No valid signing identity available to run autoUpdater specs');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.skip();
|
this.skip();
|
||||||
} else {
|
} else {
|
||||||
identity = result.stdout.toString().trim();
|
identity = result;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -40,59 +30,6 @@ ifdescribe(process.platform === 'darwin' && !(process.env.CI && process.arch ===
|
||||||
expect(identity).to.be.a('string').with.lengthOf.at.least(1);
|
expect(identity).to.be.a('string').with.lengthOf.at.least(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
const copyApp = async (newDir: string, fixture = 'initial') => {
|
|
||||||
const appBundlePath = path.resolve(process.execPath, '../../..');
|
|
||||||
const newPath = path.resolve(newDir, 'Electron.app');
|
|
||||||
cp.spawnSync('cp', ['-R', appBundlePath, path.dirname(newPath)]);
|
|
||||||
const appDir = path.resolve(newPath, 'Contents/Resources/app');
|
|
||||||
await fs.mkdirp(appDir);
|
|
||||||
await fs.copy(path.resolve(fixturesPath, 'auto-update', fixture), appDir);
|
|
||||||
const plistPath = path.resolve(newPath, 'Contents', 'Info.plist');
|
|
||||||
await fs.writeFile(
|
|
||||||
plistPath,
|
|
||||||
(await fs.readFile(plistPath, 'utf8')).replace('<key>BuildMachineOSBuild</key>', `<key>NSAppTransportSecurity</key>
|
|
||||||
<dict>
|
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
|
||||||
<true/>
|
|
||||||
<key>NSExceptionDomains</key>
|
|
||||||
<dict>
|
|
||||||
<key>localhost</key>
|
|
||||||
<dict>
|
|
||||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
|
||||||
<true/>
|
|
||||||
<key>NSIncludesSubdomains</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</dict><key>BuildMachineOSBuild</key>`)
|
|
||||||
);
|
|
||||||
return newPath;
|
|
||||||
};
|
|
||||||
|
|
||||||
const spawn = (cmd: string, args: string[], opts: any = {}) => {
|
|
||||||
let out = '';
|
|
||||||
const child = cp.spawn(cmd, args, opts);
|
|
||||||
child.stdout.on('data', (chunk: Buffer) => {
|
|
||||||
out += chunk.toString();
|
|
||||||
});
|
|
||||||
child.stderr.on('data', (chunk: Buffer) => {
|
|
||||||
out += chunk.toString();
|
|
||||||
});
|
|
||||||
return new Promise<{ code: number, out: string }>((resolve) => {
|
|
||||||
child.on('exit', (code, signal) => {
|
|
||||||
expect(signal).to.equal(null);
|
|
||||||
resolve({
|
|
||||||
code: code!,
|
|
||||||
out
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const signApp = (appPath: string) => {
|
|
||||||
return spawn('codesign', ['-s', identity, '--deep', '--force', appPath]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const launchApp = (appPath: string, args: string[] = []) => {
|
const launchApp = (appPath: string, args: string[] = []) => {
|
||||||
return spawn(path.resolve(appPath, 'Contents/MacOS/Electron'), args);
|
return spawn(path.resolve(appPath, 'Contents/MacOS/Electron'), args);
|
||||||
};
|
};
|
||||||
|
@ -107,17 +44,6 @@ ifdescribe(process.platform === 'darwin' && !(process.env.CI && process.arch ===
|
||||||
return activeShipIts;
|
return activeShipIts;
|
||||||
};
|
};
|
||||||
|
|
||||||
const withTempDirectory = async (fn: (dir: string) => Promise<void>, autoCleanUp = true) => {
|
|
||||||
const dir = await fs.mkdtemp(path.resolve(os.tmpdir(), 'electron-update-spec-'));
|
|
||||||
try {
|
|
||||||
await fn(dir);
|
|
||||||
} finally {
|
|
||||||
if (autoCleanUp) {
|
|
||||||
cp.spawnSync('rm', ['-r', dir]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const logOnError = (what: any, fn: () => void) => {
|
const logOnError = (what: any, fn: () => void) => {
|
||||||
try {
|
try {
|
||||||
fn();
|
fn();
|
||||||
|
@ -151,7 +77,7 @@ ifdescribe(process.platform === 'darwin' && !(process.env.CI && process.arch ===
|
||||||
(await fs.readFile(infoPath, 'utf8')).replace(/(<key>CFBundleShortVersionString<\/key>\s+<string>)[^<]+/g, `$1${version}`)
|
(await fs.readFile(infoPath, 'utf8')).replace(/(<key>CFBundleShortVersionString<\/key>\s+<string>)[^<]+/g, `$1${version}`)
|
||||||
);
|
);
|
||||||
await mutateAppPreSign?.mutate(secondAppPath);
|
await mutateAppPreSign?.mutate(secondAppPath);
|
||||||
await signApp(secondAppPath);
|
await signApp(secondAppPath, identity);
|
||||||
await mutateAppPostSign?.mutate(secondAppPath);
|
await mutateAppPostSign?.mutate(secondAppPath);
|
||||||
updateZipPath = path.resolve(dir, 'update.zip');
|
updateZipPath = path.resolve(dir, 'update.zip');
|
||||||
await spawn('zip', ['-0', '-r', '--symlinks', updateZipPath, './'], {
|
await spawn('zip', ['-0', '-r', '--symlinks', updateZipPath, './'], {
|
||||||
|
@ -183,7 +109,7 @@ ifdescribe(process.platform === 'darwin' && !(process.env.CI && process.arch ===
|
||||||
it('should cleanly set the feed URL when the app is signed', async () => {
|
it('should cleanly set the feed URL when the app is signed', async () => {
|
||||||
await withTempDirectory(async (dir) => {
|
await withTempDirectory(async (dir) => {
|
||||||
const appPath = await copyApp(dir);
|
const appPath = await copyApp(dir);
|
||||||
await signApp(appPath);
|
await signApp(appPath, identity);
|
||||||
const launchResult = await launchApp(appPath, ['http://myupdate']);
|
const launchResult = await launchApp(appPath, ['http://myupdate']);
|
||||||
expect(launchResult.code).to.equal(0);
|
expect(launchResult.code).to.equal(0);
|
||||||
expect(launchResult.out).to.include('Feed URL Set: http://myupdate');
|
expect(launchResult.out).to.include('Feed URL Set: http://myupdate');
|
||||||
|
@ -224,7 +150,7 @@ ifdescribe(process.platform === 'darwin' && !(process.env.CI && process.arch ===
|
||||||
it('should hit the update endpoint when checkForUpdates is called', async () => {
|
it('should hit the update endpoint when checkForUpdates is called', async () => {
|
||||||
await withTempDirectory(async (dir) => {
|
await withTempDirectory(async (dir) => {
|
||||||
const appPath = await copyApp(dir, 'check');
|
const appPath = await copyApp(dir, 'check');
|
||||||
await signApp(appPath);
|
await signApp(appPath, identity);
|
||||||
server.get('/update-check', (req, res) => {
|
server.get('/update-check', (req, res) => {
|
||||||
res.status(204).send();
|
res.status(204).send();
|
||||||
});
|
});
|
||||||
|
@ -241,7 +167,7 @@ ifdescribe(process.platform === 'darwin' && !(process.env.CI && process.arch ===
|
||||||
it('should hit the update endpoint with customer headers when checkForUpdates is called', async () => {
|
it('should hit the update endpoint with customer headers when checkForUpdates is called', async () => {
|
||||||
await withTempDirectory(async (dir) => {
|
await withTempDirectory(async (dir) => {
|
||||||
const appPath = await copyApp(dir, 'check-with-headers');
|
const appPath = await copyApp(dir, 'check-with-headers');
|
||||||
await signApp(appPath);
|
await signApp(appPath, identity);
|
||||||
server.get('/update-check', (req, res) => {
|
server.get('/update-check', (req, res) => {
|
||||||
res.status(204).send();
|
res.status(204).send();
|
||||||
});
|
});
|
||||||
|
@ -258,7 +184,7 @@ ifdescribe(process.platform === 'darwin' && !(process.env.CI && process.arch ===
|
||||||
it('should hit the download endpoint when an update is available and error if the file is bad', async () => {
|
it('should hit the download endpoint when an update is available and error if the file is bad', async () => {
|
||||||
await withTempDirectory(async (dir) => {
|
await withTempDirectory(async (dir) => {
|
||||||
const appPath = await copyApp(dir, 'update');
|
const appPath = await copyApp(dir, 'update');
|
||||||
await signApp(appPath);
|
await signApp(appPath, identity);
|
||||||
server.get('/update-file', (req, res) => {
|
server.get('/update-file', (req, res) => {
|
||||||
res.status(500).send('This is not a file');
|
res.status(500).send('This is not a file');
|
||||||
});
|
});
|
||||||
|
@ -298,7 +224,7 @@ ifdescribe(process.platform === 'darwin' && !(process.env.CI && process.arch ===
|
||||||
infoPath,
|
infoPath,
|
||||||
(await fs.readFile(infoPath, 'utf8')).replace(/(<key>CFBundleShortVersionString<\/key>\s+<string>)[^<]+/g, '$11.0.0')
|
(await fs.readFile(infoPath, 'utf8')).replace(/(<key>CFBundleShortVersionString<\/key>\s+<string>)[^<]+/g, '$11.0.0')
|
||||||
);
|
);
|
||||||
await signApp(appPath);
|
await signApp(appPath, identity);
|
||||||
|
|
||||||
const updateZipPath = await getOrCreateUpdateZipPath(opts.nextVersion, opts.endFixture, opts.mutateAppPreSign, opts.mutateAppPostSign);
|
const updateZipPath = await getOrCreateUpdateZipPath(opts.nextVersion, opts.endFixture, opts.mutateAppPreSign, opts.mutateAppPostSign);
|
||||||
|
|
||||||
|
|
22
spec/fixtures/api/fork-with-node-options.js
vendored
Normal file
22
spec/fixtures/api/fork-with-node-options.js
vendored
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
const { execFileSync } = require('node:child_process');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const fixtures = path.resolve(__dirname, '..');
|
||||||
|
|
||||||
|
const env = {
|
||||||
|
ELECTRON_RUN_AS_NODE: 'true',
|
||||||
|
// Process will exit with 1 if NODE_OPTIONS is accepted.
|
||||||
|
NODE_OPTIONS: `--require "${path.join(fixtures, 'module', 'fail.js')}"`
|
||||||
|
};
|
||||||
|
// Provide a lower cased NODE_OPTIONS in case some code ignores case sensitivity
|
||||||
|
// when reading NODE_OPTIONS.
|
||||||
|
env.node_options = env.NODE_OPTIONS;
|
||||||
|
try {
|
||||||
|
execFileSync(process.argv[2],
|
||||||
|
['--require', path.join(fixtures, 'module', 'noop.js')],
|
||||||
|
{ env, stdio: 'inherit' });
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('NODE_OPTIONS passed to child');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
99
spec/lib/codesign-helpers.ts
Normal file
99
spec/lib/codesign-helpers.ts
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
import * as cp from 'node:child_process';
|
||||||
|
import * as fs from 'fs-extra';
|
||||||
|
import * as os from 'node:os';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
|
||||||
|
const features = process._linkedBinding('electron_common_features');
|
||||||
|
const fixturesPath = path.resolve(__dirname, '..', 'fixtures');
|
||||||
|
|
||||||
|
export const shouldRunCodesignTests =
|
||||||
|
process.platform === 'darwin' &&
|
||||||
|
!(process.env.CI && process.arch === 'arm64') &&
|
||||||
|
!process.mas &&
|
||||||
|
!features.isComponentBuild();
|
||||||
|
|
||||||
|
let identity: string | null;
|
||||||
|
|
||||||
|
export function getCodesignIdentity () {
|
||||||
|
if (identity === undefined) {
|
||||||
|
const result = cp.spawnSync(path.resolve(__dirname, '../../script/codesign/get-trusted-identity.sh'));
|
||||||
|
if (result.status !== 0 || result.stdout.toString().trim().length === 0) {
|
||||||
|
// Per https://circleci.com/docs/2.0/env-vars:
|
||||||
|
// CIRCLE_PR_NUMBER is only present on forked PRs
|
||||||
|
if (process.env.CI && !process.env.CIRCLE_PR_NUMBER) {
|
||||||
|
throw new Error('No valid signing identity available to run autoUpdater specs');
|
||||||
|
}
|
||||||
|
identity = null;
|
||||||
|
} else {
|
||||||
|
identity = result.stdout.toString().trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function copyApp (newDir: string, fixture: string | null = 'initial') {
|
||||||
|
const appBundlePath = path.resolve(process.execPath, '../../..');
|
||||||
|
const newPath = path.resolve(newDir, 'Electron.app');
|
||||||
|
cp.spawnSync('cp', ['-R', appBundlePath, path.dirname(newPath)]);
|
||||||
|
if (fixture) {
|
||||||
|
const appDir = path.resolve(newPath, 'Contents/Resources/app');
|
||||||
|
await fs.mkdirp(appDir);
|
||||||
|
await fs.copy(path.resolve(fixturesPath, 'auto-update', fixture), appDir);
|
||||||
|
}
|
||||||
|
const plistPath = path.resolve(newPath, 'Contents', 'Info.plist');
|
||||||
|
await fs.writeFile(
|
||||||
|
plistPath,
|
||||||
|
(await fs.readFile(plistPath, 'utf8')).replace('<key>BuildMachineOSBuild</key>', `<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSExceptionDomains</key>
|
||||||
|
<dict>
|
||||||
|
<key>localhost</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSIncludesSubdomains</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict><key>BuildMachineOSBuild</key>`)
|
||||||
|
);
|
||||||
|
return newPath;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function spawn (cmd: string, args: string[], opts: any = {}) {
|
||||||
|
let out = '';
|
||||||
|
const child = cp.spawn(cmd, args, opts);
|
||||||
|
child.stdout.on('data', (chunk: Buffer) => {
|
||||||
|
out += chunk.toString();
|
||||||
|
});
|
||||||
|
child.stderr.on('data', (chunk: Buffer) => {
|
||||||
|
out += chunk.toString();
|
||||||
|
});
|
||||||
|
return new Promise<{ code: number, out: string }>((resolve) => {
|
||||||
|
child.on('exit', (code, signal) => {
|
||||||
|
expect(signal).to.equal(null);
|
||||||
|
resolve({
|
||||||
|
code: code!,
|
||||||
|
out
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export function signApp (appPath: string, identity: string) {
|
||||||
|
return spawn('codesign', ['-s', identity, '--deep', '--force', appPath]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function withTempDirectory (fn: (dir: string) => Promise<void>, autoCleanUp = true) {
|
||||||
|
const dir = await fs.mkdtemp(path.resolve(os.tmpdir(), 'electron-update-spec-'));
|
||||||
|
try {
|
||||||
|
await fn(dir);
|
||||||
|
} finally {
|
||||||
|
if (autoCleanUp) {
|
||||||
|
cp.spawnSync('rm', ['-r', dir]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,9 +1,10 @@
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import * as childProcess from 'node:child_process';
|
import * as childProcess from 'node:child_process';
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'fs-extra';
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
import * as util from 'node:util';
|
import * as util from 'node:util';
|
||||||
import { getRemoteContext, ifdescribe, ifit, itremote, useRemoteContext } from './lib/spec-helpers';
|
import { getRemoteContext, ifdescribe, ifit, itremote, useRemoteContext } from './lib/spec-helpers';
|
||||||
|
import { copyApp, getCodesignIdentity, shouldRunCodesignTests, signApp, spawn, withTempDirectory } from './lib/codesign-helpers';
|
||||||
import { webContents } from 'electron/main';
|
import { webContents } from 'electron/main';
|
||||||
import { EventEmitter } from 'node:stream';
|
import { EventEmitter } from 'node:stream';
|
||||||
import { once } from 'node:events';
|
import { once } from 'node:events';
|
||||||
|
@ -659,6 +660,66 @@ describe('node feature', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ifdescribe(shouldRunCodesignTests)('NODE_OPTIONS in signed app', function () {
|
||||||
|
let identity = '';
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
const result = getCodesignIdentity();
|
||||||
|
if (result === null) {
|
||||||
|
this.skip();
|
||||||
|
} else {
|
||||||
|
identity = result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const script = path.join(fixtures, 'api', 'fork-with-node-options.js');
|
||||||
|
const nodeOptionsWarning = 'NODE_OPTIONS is disabled because this process is invoked by other apps';
|
||||||
|
|
||||||
|
it('is disabled when invoked by other apps in ELECTRON_RUN_AS_NODE mode', async () => {
|
||||||
|
await withTempDirectory(async (dir) => {
|
||||||
|
const appPath = await copyApp(dir);
|
||||||
|
await signApp(appPath, identity);
|
||||||
|
// Invoke Electron by using the system node binary as middle layer, so
|
||||||
|
// the check of NODE_OPTIONS will think the process is started by other
|
||||||
|
// apps.
|
||||||
|
const { code, out } = await spawn('node', [script, path.join(appPath, 'Contents/MacOS/Electron')]);
|
||||||
|
expect(code).to.equal(0);
|
||||||
|
expect(out).to.include(nodeOptionsWarning);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is disabled when invoked by alien binary in app bundle in ELECTRON_RUN_AS_NODE mode', async function () {
|
||||||
|
await withTempDirectory(async (dir) => {
|
||||||
|
const appPath = await copyApp(dir);
|
||||||
|
await signApp(appPath, identity);
|
||||||
|
// Find system node and copy it to app bundle.
|
||||||
|
const nodePath = process.env.PATH?.split(path.delimiter).find(dir => fs.existsSync(path.join(dir, 'node')));
|
||||||
|
if (!nodePath) {
|
||||||
|
this.skip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const alienBinary = path.join(appPath, 'Contents/MacOS/node');
|
||||||
|
await fs.copy(path.join(nodePath, 'node'), alienBinary);
|
||||||
|
// Try to execute electron app from the alien node in app bundle.
|
||||||
|
const { code, out } = await spawn(alienBinary, [script, path.join(appPath, 'Contents/MacOS/Electron')]);
|
||||||
|
expect(code).to.equal(0);
|
||||||
|
expect(out).to.include(nodeOptionsWarning);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is respected when invoked from self', async () => {
|
||||||
|
await withTempDirectory(async (dir) => {
|
||||||
|
const appPath = await copyApp(dir, null);
|
||||||
|
await signApp(appPath, identity);
|
||||||
|
const appExePath = path.join(appPath, 'Contents/MacOS/Electron');
|
||||||
|
const { code, out } = await spawn(appExePath, [script, appExePath]);
|
||||||
|
expect(code).to.equal(1);
|
||||||
|
expect(out).to.not.include(nodeOptionsWarning);
|
||||||
|
expect(out).to.include('NODE_OPTIONS passed to child');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Node.js cli flags', () => {
|
describe('Node.js cli flags', () => {
|
||||||
let child: childProcess.ChildProcessWithoutNullStreams;
|
let child: childProcess.ChildProcessWithoutNullStreams;
|
||||||
let exitPromise: Promise<any[]>;
|
let exitPromise: Promise<any[]>;
|
||||||
|
|
Loading…
Reference in a new issue