diff --git a/deps/uv/include/uv/win.h b/deps/uv/include/uv/win.h index 7b4ebd4b7b2e85..c4e9e1e631dd4a 100644 --- a/deps/uv/include/uv/win.h +++ b/deps/uv/include/uv/win.h @@ -684,6 +684,7 @@ typedef struct { #define UV_FS_O_TEMPORARY _O_TEMPORARY #define UV_FS_O_TRUNC _O_TRUNC #define UV_FS_O_WRONLY _O_WRONLY +#define UV_FS_O_READLOCK 0x40000000 /* READ ONLY SHARING MODE*/ /* fs open() flags supported on other platforms (or mapped on this platform): */ #define UV_FS_O_DIRECT 0x02000000 /* FILE_FLAG_NO_BUFFERING */ diff --git a/deps/uv/src/win/fs.c b/deps/uv/src/win/fs.c index 4092de0ab31e56..c6428098151783 100644 --- a/deps/uv/src/win/fs.c +++ b/deps/uv/src/win/fs.c @@ -508,6 +508,8 @@ void fs__open(uv_fs_t* req) { */ if (flags & UV_FS_O_EXLOCK) { share = 0; + } else if (flags & UV_FS_O_READLOCK) { + share = FILE_SHARE_READ;; } else { share = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; } diff --git a/doc/api/code_integrity.md b/doc/api/code_integrity.md new file mode 100644 index 00000000000000..5d47867c94a720 --- /dev/null +++ b/doc/api/code_integrity.md @@ -0,0 +1,138 @@ +# Code Integrity + + + + + +> Stability: 1.1 - Active development + +This feature is only available on Windows platforms. + +Code integrity refers to the assurance that software code has not been +altered or tampered with in any unauthorized way. It ensures that +the code running on a system is exactly what was intended by the developers. + +Code integrity in Node.js integrates with platform features for code integrity +policy enforcement. See platform speficic sections below for more information. + +The Node.js threat model considers the code that the runtime executes to be +trusted. As such, this feature is an additional safety belt, not a strict +security boundary. + +If you find a potential security vulnerability, please refer to our +[Security Policy][]. + +## Code Integrity on Windows + +Code integrity is an opt-in feature that leverages Window Defender Application Control +to verify the code executing conforms to system policy and has not been modified since +signing time. + +There are three audiences that are involved when using Node.js in an +environment enforcing code integrity: the application developers, +those administrating the system enforcing code integrity, and +the end user. The following sections describe how each audience +can interact with code integrity enforcement. + +### Windows Code Integrity and Application Developers + +Windows Defender Application Control uses digital signatures to verify +a file's integrity. Application developers are responsible for generating and +distributing the signature information for their Node.js application. +Application developers are also expected to design their application +in robust ways to avoid unintended code execution. This includes +avoiding the use of `eval` and avoiding loading modules outside +of standard methods. + +Signature information for files which Node.js is intended to execute +can be stored in a catalog file. Application developers can generate +a Windows catalog file to store the hash of all files Node.js +is expected to execute. + +A catalog can be generated using the `New-FileCatalog` Powershell +cmdlet. For example + +```powershell +New-FileCatalog -Version 2 -CatalogFilePath MyApplicationCatalog.cat -Path \my\application\path\ +``` + +The `Path` argument should point to the root folder containing your application's code. If +your application's code is fully contained in one file, `Path` can point to that single file. + +Be sure that the catalog is generated using the final version of the files that you intend to ship +(i.e. after minifying). + +The application developer should then sign the generated catalog with their Code Signing certificate +to ensure the catalog is not tampered with between distribution and execution. + +This can be done with the [Set-AuthenticodeSignature commandlet][]. + +### Windows Code Integrity and System Administrators + +This section is intended for system administrators who want to enable Node.js +code integrity features in their environments. + +This section assumes familiarity with managing WDAC polcies. +[Official documentation for WDAC][]. + +Code integrity enforcement on Windows has two toggleable settings: +`EnforceCodeIntegrity` and `DisableInteractiveMode`. These settings are configured +by WDAC policy. + +`EnforceCodeIntegrity` causes Node.js to call WldpCanExecuteFile whenever a module is loaded using `require`. +WldpCanExecuteFile verifies that the file's integrity has not been tampered with from signing time. +The system administrator should sign and install the application's file catalog where the application +is running, per WDAC guidance. + +`DisableInteractiveMode` prevents Node.js from being run in interactive mode, and also disables the `-e` and `--eval` +command line options. + +#### Enabling Code Integrity Enforcement + +On newer Windows versions (22H2+), the preferred method of configuring application settings is done using +`AppSettings` in your WDAC Policy. + +```text + + + + True + + + True + + + +``` + +On older Windows versions, use the `Settings` section of your WDAC Policy. + +```text + + + + true + + + + + true + + + +``` + +## Code Integrity on Linux + +Code integrity on Linux is not yet implemented. Plans for implementation will +be made once the necessary APIs on Linux have been upstreamed. More information +can be found here: + +## Code Integrity on MacOS + +Code integrity on MacOS is not yet implemented. Currently, there is no +timeline for implementation. + +[Official documentation for WDAC]: https://learn.microsoft.com/en-us/windows/security/application-security/application-control/windows-defender-application-control/ +[Security Policy]: https://github.com/nodejs/node/blob/main/SECURITY.md +[Set-AuthenticodeSignature commandlet]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.security/set-authenticodesignature diff --git a/doc/api/errors.md b/doc/api/errors.md index 98073d49d62098..0e95ff3ec58a6a 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -805,6 +805,22 @@ changes: There was an attempt to use a `MessagePort` instance in a closed state, usually after `.close()` has been called. + + +### `ERR_CODE_INTEGRITY_BLOCKED` + +> Stability: 1.1 - Active development + +Feature has been disabled due to OS Code Integrity policy. + + + +### `ERR_CODE_INTEGRITY_VIOLATION` + +> Stability: 1.1 - Active development + +JavaScript code intended to be executed was rejected by system code integrity policy. + ### `ERR_CONSOLE_WRITABLE_STREAM` diff --git a/doc/api/index.md b/doc/api/index.md index fab284fe652809..5001a822b7c1d8 100644 --- a/doc/api/index.md +++ b/doc/api/index.md @@ -16,6 +16,7 @@ * [C++ embedder API](embedding.md) * [Child processes](child_process.md) * [Cluster](cluster.md) +* [Code integrity](code_integrity.md) * [Command-line options](cli.md) * [Console](console.md) * [Crypto](crypto.md) diff --git a/doc/api/wdac-manifest.xml b/doc/api/wdac-manifest.xml new file mode 100644 index 00000000000000..264de029012bf7 --- /dev/null +++ b/doc/api/wdac-manifest.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/lib/internal/code_integrity.js b/lib/internal/code_integrity.js new file mode 100644 index 00000000000000..e1ce4620fc6a44 --- /dev/null +++ b/lib/internal/code_integrity.js @@ -0,0 +1,44 @@ +// Code integrity is a security feature which prevents unsigned +// code from executing. More information can be found in the docs +// doc/api/code_integrity.md + +'use strict'; + +const { emitWarning } = require('internal/process/warning'); +const { + isFileTrustedBySystemCodeIntegrityPolicy, + isInteractiveModeDisabled, + isSystemEnforcingCodeIntegrity, +} = internalBinding('code_integrity'); + +let isCodeIntegrityEnforced; +let alreadyQueriedSystemCodeEnforcmentMode = false; + +function isAllowedToExecuteFile(filepath) { + if (!alreadyQueriedSystemCodeEnforcmentMode) { + isCodeIntegrityEnforced = isSystemEnforcingCodeIntegrity(); + + if (isCodeIntegrityEnforced) { + emitWarning( + 'Code integrity is being enforced by system policy.' + + '\nCode integrity is an experimental feature.' + + ' See docs for more info.', + 'ExperimentalWarning'); + } + + alreadyQueriedSystemCodeEnforcmentMode = true; + } + + if (!isCodeIntegrityEnforced) { + return true; + } + + return isFileTrustedBySystemCodeIntegrityPolicy(filepath); +} + +module.exports = { + isAllowedToExecuteFile, + isFileTrustedBySystemCodeIntegrityPolicy, + isInteractiveModeDisabled, + isSystemEnforcingCodeIntegrity, +}; diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 206e2a24716022..c003165005b71d 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1161,6 +1161,10 @@ E('ERR_CHILD_PROCESS_IPC_REQUIRED', Error); E('ERR_CHILD_PROCESS_STDIO_MAXBUFFER', '%s maxBuffer length exceeded', RangeError); +E('ERR_CODE_INTEGRITY_BLOCKED', + 'The feature "%s" is blocked by OS Code Integrity policy', Error); +E('ERR_CODE_INTEGRITY_VIOLATION', + 'The file %s did not pass OS Code Integrity validation', Error); E('ERR_CONSOLE_WRITABLE_STREAM', 'Console expects a writable stream instance for %s', TypeError); E('ERR_CONSTRUCT_CALL_REQUIRED', 'Class constructor %s cannot be invoked without `new`', TypeError); diff --git a/lib/internal/main/eval_string.js b/lib/internal/main/eval_string.js index 998c3fb4ada52d..7586b516e2e225 100644 --- a/lib/internal/main/eval_string.js +++ b/lib/internal/main/eval_string.js @@ -23,10 +23,24 @@ const { const { addBuiltinLibsToObject } = require('internal/modules/helpers'); const { getOptionValue } = require('internal/options'); +const { + codes: { + ERR_CODE_INTEGRITY_BLOCKED, + }, +} = require('internal/errors'); + prepareMainThreadExecution(); addBuiltinLibsToObject(globalThis, ''); markBootstrapComplete(); +const { isWindows } = require('internal/util'); +if (isWindows) { + const ci = require('internal/code_integrity'); + if (ci.isInteractiveModeDisabled()) { + throw new ERR_CODE_INTEGRITY_BLOCKED('"eval"'); + } +} + const code = getOptionValue('--eval'); const print = getOptionValue('--print'); diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 827655bedb65bf..e10e28adb9d4e2 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -190,6 +190,7 @@ const { const { codes: { + ERR_CODE_INTEGRITY_VIOLATION, ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, ERR_INVALID_MODULE_SPECIFIER, @@ -225,6 +226,11 @@ const onRequire = getLazy(() => tracingChannel('module.require')); const relativeResolveCache = { __proto__: null }; +let ci; +if (isWindows) { + ci = require('internal/code_integrity'); +} + let requireDepth = 0; let isPreloading = false; let statCache = null; @@ -1164,7 +1170,19 @@ function defaultLoadImpl(filename, format) { case 'module-typescript': case 'commonjs-typescript': case 'typescript': { - return fs.readFileSync(filename, 'utf8'); + let fd; + if (isWindows) { + fd = fs.openSync(filename, 0x40000000); + const isAllowedToExecute = ci.isAllowedToExecuteFile(fd); + if (!isAllowedToExecute) { + throw new ERR_CODE_INTEGRITY_VIOLATION(filename); + } + let source = fs.readFileSync(fd, 'utf8'); + //fs.closeSync(fd); + return source; + } else { + return fs.readFileSync(filename, 'utf8'); + } } case 'builtin': return null; @@ -1268,6 +1286,13 @@ Module._load = function(request, parent, isMain, internalResolveOptions = kEmpty // TODO(joyeecheung): a more sensible handling is probably, if there are hooks, always go through the hooks // first before checking the cache. Otherwise, check the cache first, then proceed to default loading. if (request === url && StringPrototypeStartsWith(request, 'node:')) { + if (isWindows) { + const isAllowedToExecute = ci.isAllowedToExecuteFile(filename); + if (!isAllowedToExecute) { + throw new ERR_CODE_INTEGRITY_VIOLATION(filename); + } + } + const normalized = BuiltinModule.normalizeRequirableId(request); if (normalized) { // It's a builtin module. const { resultFromHook, builtinExports } = loadBuiltinWithHooks(normalized, url, format); @@ -1309,6 +1334,13 @@ Module._load = function(request, parent, isMain, internalResolveOptions = kEmpty } if (resultFromLoadHook === undefined && BuiltinModule.canBeRequiredWithoutScheme(filename)) { + if (isWindows) { + const isAllowedToExecute = ci.isAllowedToExecuteFile(filename); + if (!isAllowedToExecute) { + throw new ERR_CODE_INTEGRITY_VIOLATION(filename); + } + } + const { resultFromHook, builtinExports } = loadBuiltinWithHooks(filename, url, format); if (builtinExports) { return builtinExports; diff --git a/lib/internal/modules/esm/load.js b/lib/internal/modules/esm/load.js index c284163fba86ec..88d5554b6d4c9a 100644 --- a/lib/internal/modules/esm/load.js +++ b/lib/internal/modules/esm/load.js @@ -4,17 +4,20 @@ const { RegExpPrototypeExec, } = primordials; const { + isWindows, kEmptyObject, } = require('internal/util'); const { defaultGetFormat } = require('internal/modules/esm/get_format'); const { validateAttributes, emitImportAssertionWarning } = require('internal/modules/esm/assert'); -const { readFileSync } = require('fs'); +const { closeSync, openSync, readFileSync } = require('fs'); +const { O_EXCL } = require('fs').constants; const { Buffer: { from: BufferFrom } } = require('buffer'); -const { URL } = require('internal/url'); +const { URL, fileURLToPath } = require('internal/url'); const { + ERR_CODE_INTEGRITY_VIOLATION, ERR_INVALID_URL, ERR_UNKNOWN_MODULE_FORMAT, ERR_UNSUPPORTED_ESM_URL_SCHEME, @@ -24,6 +27,11 @@ const { dataURLProcessor, } = require('internal/data_url'); +let ci; +if (isWindows) { + ci = require('internal/code_integrity'); +} + /** * @param {URL} url URL to the module * @param {LoadContext} context used to decorate error messages @@ -33,8 +41,23 @@ function getSourceSync(url, context) { const { protocol, href } = url; const responseURL = href; let source; + let fd; if (protocol === 'file:') { - source = readFileSync(url); + if (isWindows) { + let filePath = fileURLToPath(url); + fd = openSync(filePath, 0x40000000); + + const isAllowedToExecute = ci.isAllowedToExecuteFile(fd); + if (!isAllowedToExecute) { + throw new ERR_CODE_INTEGRITY_VIOLATION(url); + } + source = readFileSync(fd); + closeSync(fd); + } + else + { + source = readFileSync(filePath); + } } else if (protocol === 'data:') { const result = dataURLProcessor(url); if (result === 'failure') { diff --git a/node.gyp b/node.gyp index ed699e0d4c03f1..e66ea033c99707 100644 --- a/node.gyp +++ b/node.gyp @@ -246,6 +246,7 @@ 'src/node_blob.h', 'src/node_buffer.h', 'src/node_builtins.h', + 'src/node_code_integrity.h', 'src/node_config_file.h', 'src/node_constants.h', 'src/node_context_data.h', @@ -491,6 +492,14 @@ }, { 'use_openssl_def%': 0, }], + # Only compile node_code_integrity on Windows + [ 'OS=="win"', { + 'node_sources': [ + '<(node_sources)', + 'src/node_code_integrity.cc', + 'src/node_code_integrity.h', + ], + }], ], }, diff --git a/src/node_binding.cc b/src/node_binding.cc index b76ecc8cab47df..8cc1e49413df77 100644 --- a/src/node_binding.cc +++ b/src/node_binding.cc @@ -100,6 +100,12 @@ V(worker) \ V(zlib) +#define NODE_BUILTIN_OS_SPECIFIC_BINDINGS(V) + +#ifdef _WIN32 +#define NODE_BUILTIN_OS_SPECIFIC_BINDINGS(V) V(code_integrity) +#endif + #define NODE_BUILTIN_BINDINGS(V) \ NODE_BUILTIN_STANDARD_BINDINGS(V) \ NODE_BUILTIN_OPENSSL_BINDINGS(V) \ @@ -107,7 +113,8 @@ NODE_BUILTIN_PROFILER_BINDINGS(V) \ NODE_BUILTIN_DEBUG_BINDINGS(V) \ NODE_BUILTIN_QUIC_BINDINGS(V) \ - NODE_BUILTIN_SQLITE_BINDINGS(V) + NODE_BUILTIN_SQLITE_BINDINGS(V) \ + NODE_BUILTIN_OS_SPECIFIC_BINDINGS(V) // This is used to load built-in bindings. Instead of using // __attribute__((constructor)), we call the _register_ diff --git a/src/node_builtins.cc b/src/node_builtins.cc index b02252540d1f3b..7a2cb002d689f8 100644 --- a/src/node_builtins.cc +++ b/src/node_builtins.cc @@ -151,6 +151,10 @@ BuiltinLoader::BuiltinCategories BuiltinLoader::GetBuiltinCategories() const { "internal/inspector/webstorage", #endif "internal/test/binding", "internal/v8_prof_polyfill", + "internal/v8_prof_processor", +#if !_WIN32 + "internal/code_integrity", // Only implemented on Windows +#endif }; auto source = source_.read(); diff --git a/src/node_code_integrity.cc b/src/node_code_integrity.cc new file mode 100644 index 00000000000000..e5d4ee0561f090 --- /dev/null +++ b/src/node_code_integrity.cc @@ -0,0 +1,295 @@ +#ifdef _WIN32 + +#include "node_code_integrity.h" +#include "env-inl.h" +#include "node.h" +#include "node_errors.h" +#include "node_external_reference.h" +#include "util.h" +#include "uv.h" +#include "v8.h" + +namespace node { + +using v8::Boolean; +using v8::Context; +using v8::FunctionCallbackInfo; +using v8::Local; +using v8::Object; +using v8::Value; + +namespace per_process { +bool isWldpInitialized = false; + +// WldpCanExecuteFile queries system code integrity policy +// to determine if the contents of a file are allowed to be executed. +pfnWldpCanExecuteFile WldpCanExecuteFile; + +// WldpGetApplicationSettingBoolean queries system code integrity policy +// for an arbitrary flag. NodeJS uses the "Node.js EnforceCodeIntegrity" +// flag to determine if NodeJS should be calling WldpCanExecuteFile +// on files intended for execution +// NodeJS also uses the "Node.js DisableInteractiveMode" flag to determine +// if it should restrict interactive code execution. More details +// on how to configure these flags can be found in doc/api/code_integrity.md +pfnWldpGetApplicationSettingBoolean WldpGetApplicationSettingBoolean; + +// WldpQuerySecurityPolicy performs similar functionality to +// WldpGetApplicationSettingBoolean, except for legacy Windows systems. +// WldpGetApplicationSettingBoolean was introduced Win10 2023H2, +// and is the modern API. However, to support more Node users, +// we also fall back to WldpQuerySecurityPolicy, +// which is available on Windows systems back to Win10 RS2 +pfnWldpQuerySecurityPolicy WldpQuerySecurityPolicy; +} // namespace per_process + +namespace code_integrity { + +static PCWSTR NODEJS = L"Node.js"; +static PCWSTR ENFORCE_CODE_INTEGRITY_SETTING_NAME = L"EnforceCodeIntegrity"; +static PCWSTR DISABLE_INTERPRETIVE_MODE_SETTING_NAME = + L"DisableInteractiveMode"; + +// InitWldp loads WLDP.dll (the Windows code integrity for interpreters DLL) +// and the relevant function pointers +void InitWldp(Environment* env) { + if (per_process::isWldpInitialized) { + return; + } + + HMODULE wldp_module = + LoadLibraryExA("wldp.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); + + if (wldp_module == nullptr) { + // Wldp is included on all Windows systems that are supported by Node.js + // If Wldp is unable to be loaded, something is very wrong with + // the system state + THROW_ERR_INVALID_STATE(env, "WLDP.DLL does not exist"); + return; + } + + per_process::WldpCanExecuteFile = + (pfnWldpCanExecuteFile)GetProcAddress(wldp_module, "WldpCanExecuteFile"); + + per_process::WldpGetApplicationSettingBoolean = + (pfnWldpGetApplicationSettingBoolean)GetProcAddress( + wldp_module, "WldpGetApplicationSettingBoolean"); + + per_process::WldpQuerySecurityPolicy = + (pfnWldpQuerySecurityPolicy)GetProcAddress(wldp_module, + "WldpQuerySecurityPolicy"); + + per_process::isWldpInitialized = true; +} + +// IsFileTrustedBySystemCodeIntegrityPolicy +// Queries operating system to determine if the contents of a file are +// allowed to be executed according to system code integrity policy. +static void IsFileTrustedBySystemCodeIntegrityPolicy( + const FunctionCallbackInfo& args) { + CHECK_EQ(args.Length(), 1); + + CHECK(args[0]->IsInt32()); + const int fd = args[0].As()->Value(); + + Environment* env = Environment::GetCurrent(args); + if (!per_process::isWldpInitialized) { + InitWldp(env); + } + + // BufferValue path(env->isolate(), args[0]); + // CHECK_NOT_NULL(*path); + + // HANDLE hFile = CreateFileA(*path, + // GENERIC_READ, + // FILE_SHARE_READ, + // nullptr, + // OPEN_EXISTING, + // FILE_ATTRIBUTE_NORMAL, + // nullptr); + + // if (hFile == INVALID_HANDLE_VALUE || hFile == nullptr) { + // return args.GetReturnValue().SetFalse(); + // } + + HANDLE hFile = uv_get_osfhandle(fd); + + if (hFile == INVALID_HANDLE_VALUE || hFile == nullptr) { + return args.GetReturnValue().SetFalse(); + } + + const GUID wldp_host_other = WLDP_HOST_OTHER; + WLDP_EXECUTION_POLICY result; + HRESULT hr = + per_process::WldpCanExecuteFile(wldp_host_other, + WLDP_EXECUTION_EVALUATION_OPTION_NONE, + hFile, + NODEJS, + &result); + + if (FAILED(hr)) { + // The failure cases from WldpCanExecuteFile are generally + // not recoverable. Inspection of the Windows event logs is necessary. + // The secure failure mode is not executing the file + args.GetReturnValue().SetFalse(); + return; + } + + bool isFileTrusted = (result == WLDP_EXECUTION_POLICY_ALLOWED); + args.GetReturnValue().Set(isFileTrusted); +} + +// IsInteractiveModeDisabled +// Queries operating system code integrity policy to determine if +// the policy is requesting NodeJS to disable interactive mode. +static void IsInteractiveModeDisabled(const FunctionCallbackInfo& args) { + CHECK_EQ(args.Length(), 0); + + Environment* env = Environment::GetCurrent(args); + + if (!per_process::isWldpInitialized) { + InitWldp(env); + } + + if (per_process::WldpGetApplicationSettingBoolean != nullptr) { + bool isInteractiveModeDisabled; + HRESULT hr = per_process::WldpGetApplicationSettingBoolean( + NODEJS, + DISABLE_INTERPRETIVE_MODE_SETTING_NAME, + &isInteractiveModeDisabled); + + if (SUCCEEDED(hr)) { + args.GetReturnValue().Set(isInteractiveModeDisabled); + return; + } else if (hr != E_NOTFOUND) { + // If the setting is not found, continue through to attempt + // WldpQuerySecurityPolicy, as the setting may be defined + // in the old settings format + args.GetReturnValue().SetFalse(); + return; + } + } + + // WldpGetApplicationSettingBoolean is the preferred way for applications to + // query security policy values. However, this method only exists on Windows + // versions going back to circa Win10 2023H2. In order to support systems + // older than that (down to Win10RS2), we can use the deprecated + // WldpQuerySecurityPolicy + if (per_process::WldpQuerySecurityPolicy != nullptr) { + DECLARE_CONST_UNICODE_STRING(providerName, L"Node.js"); + DECLARE_CONST_UNICODE_STRING(keyName, L"Settings"); + DECLARE_CONST_UNICODE_STRING(valueName, L"DisableInteractiveMode"); + WLDP_SECURE_SETTING_VALUE_TYPE valueType = + WLDP_SECURE_SETTING_VALUE_TYPE_BOOLEAN; + ULONG valueSize = sizeof(int); + int isInteractiveModeDisabled = 0; + HRESULT hr = + per_process::WldpQuerySecurityPolicy(&providerName, + &keyName, + &valueName, + &valueType, + &isInteractiveModeDisabled, + &valueSize); + + if (FAILED(hr)) { + args.GetReturnValue().SetFalse(); + return; + } + + args.GetReturnValue().Set(Boolean::New( + env->isolate(), static_cast(isInteractiveModeDisabled))); + } +} + +// IsSystemEnforcingCodeIntegrity +// Queries the operating system to determine if NodeJS should be enforcing +// integrity checks by calling WldpCanExecuteFile +static void IsSystemEnforcingCodeIntegrity( + const FunctionCallbackInfo& args) { + CHECK_EQ(args.Length(), 0); + + Environment* env = Environment::GetCurrent(args); + + if (!per_process::isWldpInitialized) { + InitWldp(env); + } + + if (per_process::WldpGetApplicationSettingBoolean != nullptr) { + bool isCodeIntegrityEnforced; + HRESULT hr = per_process::WldpGetApplicationSettingBoolean( + NODEJS, ENFORCE_CODE_INTEGRITY_SETTING_NAME, &isCodeIntegrityEnforced); + + if (SUCCEEDED(hr)) { + args.GetReturnValue().Set(isCodeIntegrityEnforced); + return; + } else if (hr != E_NOTFOUND) { + // If the setting is not found, continue through to attempt + // WldpQuerySecurityPolicy, as the setting may be defined + // in the old settings format + args.GetReturnValue().SetFalse(); + return; + } + } + + // WldpGetApplicationSettingBoolean is the preferred way for applications to + // query security policy values. However, this method only exists on Windows + // versions going back to circa Win10 2023H2. In order to support systems + // older than that (down to Win10RS2), we can use the deprecated + // WldpQuerySecurityPolicy + if (per_process::WldpQuerySecurityPolicy != nullptr) { + DECLARE_CONST_UNICODE_STRING(providerName, L"Node.js"); + DECLARE_CONST_UNICODE_STRING(keyName, L"Settings"); + DECLARE_CONST_UNICODE_STRING(valueName, L"EnforceCodeIntegrity"); + WLDP_SECURE_SETTING_VALUE_TYPE valueType = + WLDP_SECURE_SETTING_VALUE_TYPE_BOOLEAN; + ULONG valueSize = sizeof(int); + int isCodeIntegrityEnforced = 0; + HRESULT hr = per_process::WldpQuerySecurityPolicy(&providerName, + &keyName, + &valueName, + &valueType, + &isCodeIntegrityEnforced, + &valueSize); + + if (FAILED(hr)) { + args.GetReturnValue().SetFalse(); + return; + } + + args.GetReturnValue().Set(Boolean::New( + env->isolate(), static_cast(isCodeIntegrityEnforced))); + } +} + +void Initialize(Local target, + Local unused, + Local context, + void* priv) { + SetMethod(context, + target, + "isFileTrustedBySystemCodeIntegrityPolicy", + IsFileTrustedBySystemCodeIntegrityPolicy); + + SetMethod( + context, target, "isInteractiveModeDisabled", IsInteractiveModeDisabled); + + SetMethod(context, + target, + "isSystemEnforcingCodeIntegrity", + IsSystemEnforcingCodeIntegrity); +} + +void RegisterExternalReferences(ExternalReferenceRegistry* registry) { + registry->Register(IsFileTrustedBySystemCodeIntegrityPolicy); + registry->Register(IsInteractiveModeDisabled); + registry->Register(IsSystemEnforcingCodeIntegrity); +} + +} // namespace code_integrity +} // namespace node + +NODE_BINDING_CONTEXT_AWARE_INTERNAL(code_integrity, + node::code_integrity::Initialize) +NODE_BINDING_EXTERNAL_REFERENCE( + code_integrity, node::code_integrity::RegisterExternalReferences) +#endif // _WIN32 diff --git a/src/node_code_integrity.h b/src/node_code_integrity.h new file mode 100644 index 00000000000000..001bc8611e59bd --- /dev/null +++ b/src/node_code_integrity.h @@ -0,0 +1,90 @@ +// Windows API documentation for WLDP can be found at +// https://learn.microsoft.com/en-us/windows/win32/api/wldp/ +#ifdef _WIN32 + +#ifndef SRC_NODE_CODE_INTEGRITY_H_ +#define SRC_NODE_CODE_INTEGRITY_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include + +#define WLDP_HOST_OTHER \ + {0x626cbec3, 0xe1fa, 0x4227, {0x98, 0x0, 0xed, 0x21, 0x2, 0x74, 0xcf, 0x7c}}; + +// +// Enumeration types for WldpCanExecuteFile +// +typedef enum WLDP_EXECUTION_POLICY { + WLDP_EXECUTION_POLICY_BLOCKED, + WLDP_EXECUTION_POLICY_ALLOWED, + WLDP_EXECUTION_POLICY_REQUIRE_SANDBOX, +} WLDP_EXECUTION_POLICY; + +typedef enum WLDP_EXECUTION_EVALUATION_OPTIONS { + WLDP_EXECUTION_EVALUATION_OPTION_NONE = 0x0, + WLDP_EXECUTION_EVALUATION_OPTION_EXECUTE_IN_INTERACTIVE_SESSION = 0x1, +} WLDP_EXECUTION_EVALUATION_OPTIONS; + +typedef HRESULT(WINAPI* pfnWldpCanExecuteFile)( + _In_ REFGUID host, + _In_ WLDP_EXECUTION_EVALUATION_OPTIONS options, + _In_ HANDLE contentFileHandle, + _In_opt_ PCWSTR auditInfo, + _Out_ WLDP_EXECUTION_POLICY* result); + +typedef HRESULT(WINAPI* pfnWldpCanExecuteBuffer)( + _In_ REFGUID host, + _In_ WLDP_EXECUTION_EVALUATION_OPTIONS options, + _In_reads_(bufferSize) const BYTE* buffer, + _In_ ULONG bufferSize, + _In_opt_ PCWSTR auditInfo, + _Out_ WLDP_EXECUTION_POLICY* result); + +typedef HRESULT(WINAPI* pfnWldpGetApplicationSettingBoolean)( + _In_ PCWSTR id, _In_ PCWSTR setting, _Out_ bool* result); + +typedef enum WLDP_SECURE_SETTING_VALUE_TYPE { + WLDP_SECURE_SETTING_VALUE_TYPE_BOOLEAN = 0, + WLDP_SECURE_SETTING_VALUE_TYPE_ULONG, + WLDP_SECURE_SETTING_VALUE_TYPE_BINARY, + WLDP_SECURE_SETTING_VALUE_TYPE_STRING +} WLDP_SECURE_SETTING_VALUE_TYPE, + *PWLDP_SECURE_SETTING_VALUE_TYPE; + +/* from winternl.h */ +#if !defined(__UNICODE_STRING_DEFINED) && defined(__MINGW32__) +#define __UNICODE_STRING_DEFINED +#endif +typedef struct _UNICODE_STRING { + USHORT Length; + USHORT MaximumLength; + PWSTR Buffer; +} UNICODE_STRING, *PUNICODE_STRING; + +typedef const UNICODE_STRING* PCUNICODE_STRING; + +typedef HRESULT(WINAPI* pfnWldpQuerySecurityPolicy)( + _In_ const UNICODE_STRING* providerName, + _In_ const UNICODE_STRING* keyName, + _In_ const UNICODE_STRING* valueName, + _Out_ PWLDP_SECURE_SETTING_VALUE_TYPE valueType, + _Out_writes_bytes_opt_(*valueSize) PVOID valueAddress, + _Inout_ PULONG valueSize); + +#ifndef DECLARE_CONST_UNICODE_STRING +#define DECLARE_CONST_UNICODE_STRING(_var, _string) \ + const WCHAR _var##_buffer[] = _string; \ + const UNICODE_STRING _var = { \ + sizeof(_string) - sizeof(WCHAR), sizeof(_string), (PWCH)_var##_buffer} +#endif + +#ifndef E_NOTFOUND +#define E_NOTFOUND 0x80070490 +#endif + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#endif // _WIN32 + +#endif // SRC_NODE_CODE_INTEGRITY_H_ diff --git a/src/node_external_reference.h b/src/node_external_reference.h index 5ce9ec0a8c207b..af09f18506e736 100644 --- a/src/node_external_reference.h +++ b/src/node_external_reference.h @@ -144,12 +144,19 @@ class ExternalReferenceRegistry { #define EXTERNAL_REFERENCE_BINDING_LIST_QUIC(V) #endif +#define EXTERNAL_REFERENCE_BINDING_LIST_OS_SPECIFIC(V) + +#ifdef _WIN32 +#define EXTERNAL_REFERENCE_BINDING_LIST_OS_SPECIFIC(V) V(code_integrity) +#endif + #define EXTERNAL_REFERENCE_BINDING_LIST(V) \ EXTERNAL_REFERENCE_BINDING_LIST_BASE(V) \ EXTERNAL_REFERENCE_BINDING_LIST_INSPECTOR(V) \ EXTERNAL_REFERENCE_BINDING_LIST_I18N(V) \ EXTERNAL_REFERENCE_BINDING_LIST_CRYPTO(V) \ - EXTERNAL_REFERENCE_BINDING_LIST_QUIC(V) + EXTERNAL_REFERENCE_BINDING_LIST_QUIC(V) \ + EXTERNAL_REFERENCE_BINDING_LIST_OS_SPECIFIC(V) } // namespace node diff --git a/test/fixtures/code_integrity_test.js b/test/fixtures/code_integrity_test.js new file mode 100644 index 00000000000000..839ca115b48d19 --- /dev/null +++ b/test/fixtures/code_integrity_test.js @@ -0,0 +1 @@ +1 + 1; diff --git a/test/fixtures/code_integrity_test.json b/test/fixtures/code_integrity_test.json new file mode 100644 index 00000000000000..0967ef424bce67 --- /dev/null +++ b/test/fixtures/code_integrity_test.json @@ -0,0 +1 @@ +{} diff --git a/test/fixtures/code_integrity_test.node b/test/fixtures/code_integrity_test.node new file mode 100644 index 00000000000000..af84f6510f0d90 --- /dev/null +++ b/test/fixtures/code_integrity_test.node @@ -0,0 +1 @@ +exports.file1 = 'file1.node'; diff --git a/test/fixtures/code_integrity_test2.js b/test/fixtures/code_integrity_test2.js new file mode 100644 index 00000000000000..c1c6922d1dfea3 --- /dev/null +++ b/test/fixtures/code_integrity_test2.js @@ -0,0 +1 @@ +return true; diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index 92bf3be1f612ff..07f73eba9291ad 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -112,6 +112,13 @@ expected.beforePreExec = new Set([ 'NativeModule internal/data_url', 'NativeModule internal/mime', 'NativeModule internal/modules/esm/utils', +]); +if (common.isWindows) { + expected.beforePreExec.add('NativeModule internal/code_integrity'); + expected.beforePreExec.add('Internal Binding code_integrity'); +} + +expected.atRunTime = new Set([ 'Internal Binding worker', 'NativeModule internal/modules/run_main', 'NativeModule internal/net', diff --git a/test/parallel/test-code-integrity.js b/test/parallel/test-code-integrity.js new file mode 100644 index 00000000000000..9bf7d24bc22a9b --- /dev/null +++ b/test/parallel/test-code-integrity.js @@ -0,0 +1,88 @@ +// Flags: --expose-internals + +'use strict'; + +const common = require('../common'); +const assert = require('node:assert'); +const { describe, it } = require('node:test'); + +// This functionality is currently only on Windows +if (!common.isWindows) { + common.skip('Windows specific test.'); +} + +const ci = require('internal/code_integrity'); + +describe('cjs loader code integrity integration tests', () => { + it('should throw an error if a .js file does not pass code integrity policy', + (t) => { + t.mock.method(ci, ci.isAllowedToExecuteFile.name, () => { return false; }); + + assert.throws( + () => { + require('../fixtures/code_integrity_test.js'); + }, + { + code: 'ERR_CODE_INTEGRITY_VIOLATION', + }, + ); + } + ); + it('should NOT throw an error if a .js file passes code integrity policy', + (t) => { + t.mock.method(ci, ci.isAllowedToExecuteFile.name, () => { return true; }); + + assert.ok( + require('../fixtures/code_integrity_test.js') + ); + } + ); + it('should throw an error if a .json file does not pass code integrity policy', + (t) => { + t.mock.method(ci, ci.isAllowedToExecuteFile.name, () => { return false; }); + + assert.throws( + () => { + require('../fixtures/code_integrity_test.json'); + }, + { + code: 'ERR_CODE_INTEGRITY_VIOLATION', + }, + ); + } + ); + it('should NOT throw an error if a .json file passes code integrity policy', + (t) => { + t.mock.method(ci, ci.isAllowedToExecuteFile.name, () => { return true; }); + + assert.ok( + require('../fixtures/code_integrity_test.json') + ); + } + ); +}); + +describe('esm loader code integrity integration tests', async () => { + it('should NOT throw an error if a file passes code integrity policy', + async (t) => { + t.mock.method(ci, ci.isAllowedToExecuteFile.name, () => { return true; }); + + // This should import without throwing ERR_CODE_INTEGRITY_VIOLATION + await import('../fixtures/code_integrity_test.js'); + } + ); + + it('should throw an error if a file does not pass code integrity policy', + async (t) => { + t.mock.method(ci, ci.isAllowedToExecuteFile.name, () => { return false; }); + try { + await import('../fixtures/code_integrity_test2.js'); + } catch (e) { + assert.strictEqual(e.code, 'ERR_CODE_INTEGRITY_VIOLATION'); + return; + } + + assert.fail('No exception thrown'); + } + ); +}); diff --git a/typings/globals.d.ts b/typings/globals.d.ts index ddd5885faa057f..0aaa000dd11145 100644 --- a/typings/globals.d.ts +++ b/typings/globals.d.ts @@ -3,6 +3,7 @@ import { AsyncWrapBinding } from './internalBinding/async_wrap'; import { BlobBinding } from './internalBinding/blob'; import { BufferBinding } from './internalBinding/buffer'; import { CJSLexerBinding } from './internalBinding/cjs_lexer'; +import { CodeIntegrityBinding } from './internalBinding/code_integrity'; import { ConfigBinding } from './internalBinding/config'; import { ConstantsBinding } from './internalBinding/constants'; import { DebugBinding } from './internalBinding/debug'; @@ -38,6 +39,7 @@ interface InternalBindingMap { blob: BlobBinding; buffer: BufferBinding; cjs_lexer: CJSLexerBinding; + code_integrity: CodeIntegrityBinding; config: ConfigBinding; constants: ConstantsBinding; debug: DebugBinding; diff --git a/typings/internalBinding/code_integrity.d.ts b/typings/internalBinding/code_integrity.d.ts new file mode 100644 index 00000000000000..6ed628180c815d --- /dev/null +++ b/typings/internalBinding/code_integrity.d.ts @@ -0,0 +1,6 @@ +export interface CodeIntegrityBinding { + isAllowedToExecuteFile(filePath: string) : boolean; + isFileTrustedBySystemCodeIntegrityPolicy(filePath: string) : boolean; + isInteractiveModeDisabled() : boolean; + isSystemEnforcingCodeIntegrity() : boolean; +}