From 8b74614e495b33bcf3dd24d7db2de46e08b4fc5c Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Mon, 24 Feb 2025 13:16:30 -0800 Subject: [PATCH] src: add WDAC integration (Windows) Add calls to Windows Defender Application Control to enforce integrity of .js, .json, .node files. add typings. fix version number add integrity checks to esm loader fix esm code integrity tests only load code integrity module on Windows address typings feedback fix readability and formatting fix naming conventions fix error in merge fix formatting properly load isWindows fix formatting remove code_integrity from builtins on non windows fix spacing before comment only load code integrity module on Windows clarify docs fix comment fix ordering of links --- doc/api/code_integrity.md | 138 ++++++++++ doc/api/errors.md | 16 ++ doc/api/index.md | 1 + doc/api/wdac-manifest.xml | 7 + lib/internal/code_integrity.js | 44 +++ lib/internal/errors.js | 4 + lib/internal/main/eval_string.js | 14 + lib/internal/modules/cjs/loader.js | 20 ++ lib/internal/modules/esm/load.js | 15 +- node.gyp | 9 + src/node_binding.cc | 9 +- src/node_builtins.cc | 3 + src/node_code_integrity.cc | 287 ++++++++++++++++++++ src/node_code_integrity.h | 90 ++++++ src/node_external_reference.h | 9 +- test/fixtures/code_integrity_test.js | 1 + test/fixtures/code_integrity_test.json | 1 + test/fixtures/code_integrity_test.node | 1 + test/fixtures/code_integrity_test2.js | 1 + test/parallel/test-bootstrap-modules.js | 4 + test/parallel/test-code-integrity.js | 102 +++++++ typings/globals.d.ts | 2 + typings/internalBinding/code_integrity.d.ts | 6 + 23 files changed, 781 insertions(+), 3 deletions(-) create mode 100644 doc/api/code_integrity.md create mode 100644 doc/api/wdac-manifest.xml create mode 100644 lib/internal/code_integrity.js create mode 100644 src/node_code_integrity.cc create mode 100644 src/node_code_integrity.h create mode 100644 test/fixtures/code_integrity_test.js create mode 100644 test/fixtures/code_integrity_test.json create mode 100644 test/fixtures/code_integrity_test.node create mode 100644 test/fixtures/code_integrity_test2.js create mode 100644 test/parallel/test-code-integrity.js create mode 100644 typings/internalBinding/code_integrity.d.ts 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 b06abbf8d96a2a..1bf328a2bf57eb 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -794,6 +794,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 7b4144639a07be..f31f753d8fb12d 100644 --- a/doc/api/index.md +++ b/doc/api/index.md @@ -19,6 +19,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 3b12f2e5551c40..19d2c9a7133c5a 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1157,6 +1157,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_CONTEXT_NOT_INITIALIZED', 'context used is not initialized', Error); diff --git a/lib/internal/main/eval_string.js b/lib/internal/main/eval_string.js index ee402f50fbdd2b..dd37c20d4665f7 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 67ec47c424fbc4..b8e3b5a67da17f 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -181,6 +181,7 @@ const { const { codes: { + ERR_CODE_INTEGRITY_VIOLATION, ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, ERR_INVALID_MODULE_SPECIFIER, @@ -216,6 +217,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; @@ -1180,6 +1186,13 @@ Module._load = function(request, parent, isMain) { // For backwards compatibility, if the request itself starts with node:, load it before checking // Module._cache. Otherwise, load it after the check. if (StringPrototypeStartsWith(request, 'node:')) { + + if (isWindows) { + const isAllowedToExecute = ci.isAllowedToExecuteFile(filename); + if (!isAllowedToExecute) { + throw new ERR_CODE_INTEGRITY_VIOLATION(filename); + } + } const result = loadBuiltinWithHooks(filename, url, format); if (result) { return result; @@ -1210,6 +1223,13 @@ Module._load = function(request, parent, isMain) { cachedModule[kModuleCircularVisited] = true; } + if (isWindows) { + const isAllowedToExecute = ci.isAllowedToExecuteFile(filename); + if (!isAllowedToExecute) { + throw new ERR_CODE_INTEGRITY_VIOLATION(filename); + } + } + if (BuiltinModule.canBeRequiredWithoutScheme(filename)) { const result = loadBuiltinWithHooks(filename, url, format); if (result) { diff --git a/lib/internal/modules/esm/load.js b/lib/internal/modules/esm/load.js index 98e14455075d2f..9c15256f72ada9 100644 --- a/lib/internal/modules/esm/load.js +++ b/lib/internal/modules/esm/load.js @@ -4,6 +4,7 @@ const { RegExpPrototypeExec, } = primordials; const { + isWindows, kEmptyObject, } = require('internal/util'); @@ -13,8 +14,9 @@ const { readFileSync } = require('fs'); 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 +26,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 @@ -34,6 +41,12 @@ function getSourceSync(url, context) { const responseURL = href; let source; if (protocol === 'file:') { + if (isWindows) { + const isAllowedToExecute = ci.isAllowedToExecuteFile(fileURLToPath(url)); + if (!isAllowedToExecute) { + throw new ERR_CODE_INTEGRITY_VIOLATION(url); + } + } source = readFileSync(url); } else if (protocol === 'data:') { const result = dataURLProcessor(url); diff --git a/node.gyp b/node.gyp index 64c1e9cdd99256..b6ae01e3a1d6e2 100644 --- a/node.gyp +++ b/node.gyp @@ -232,6 +232,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', @@ -455,6 +456,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 367a5bcd402b53..5a30a130022725 100644 --- a/src/node_binding.cc +++ b/src/node_binding.cc @@ -97,6 +97,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) \ @@ -104,7 +110,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 00a4ef69d31884..5010a337ebdc7a 100644 --- a/src/node_builtins.cc +++ b/src/node_builtins.cc @@ -150,6 +150,9 @@ BuiltinLoader::BuiltinCategories BuiltinLoader::GetBuiltinCategories() const { #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..2ec8bc2a41e055 --- /dev/null +++ b/src/node_code_integrity.cc @@ -0,0 +1,287 @@ +#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 "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]->IsString()); + + 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(); + } + + 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); + CloseHandle(hFile); + + 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 5981e9db9c3bc4..f6c03523419a2f 100644 --- a/src/node_external_reference.h +++ b/src/node_external_reference.h @@ -241,12 +241,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 caf3b315f78872..d0c951b03be524 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -108,6 +108,10 @@ expected.beforePreExec = new Set([ 'NativeModule internal/data_url', 'NativeModule internal/mime', ]); +if (common.isWindows) { + expected.beforePreExec.add('NativeModule internal/code_integrity'); + expected.beforePreExec.add('Internal Binding code_integrity'); +} expected.atRunTime = new Set([ 'Internal Binding worker', diff --git a/test/parallel/test-code-integrity.js b/test/parallel/test-code-integrity.js new file mode 100644 index 00000000000000..bfc709ec55b95f --- /dev/null +++ b/test/parallel/test-code-integrity.js @@ -0,0 +1,102 @@ +// 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') + ); + } + ); + it('should throw an error if a .node file does not pass code integrity policy', + (t) => { + t.mock.method(ci, ci.isAllowedToExecuteFile.name, () => { return false; }); + + assert.throws( + () => { + require('../fixtures/code_integrity_test.node'); + }, + { + code: 'ERR_CODE_INTEGRITY_VIOLATION', + }, + ); + } + ); +}); + +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 1bd3f46d0e2567..707556af0b02f7 100644 --- a/typings/globals.d.ts +++ b/typings/globals.d.ts @@ -1,5 +1,6 @@ import { AsyncWrapBinding } from './internalBinding/async_wrap'; import { BlobBinding } from './internalBinding/blob'; +import { CodeIntegrityBinding } from './internalBinding/code_integrity'; import { ConfigBinding } from './internalBinding/config'; import { ConstantsBinding } from './internalBinding/constants'; import { DebugBinding } from './internalBinding/debug'; @@ -26,6 +27,7 @@ import { ZlibBinding } from './internalBinding/zlib'; interface InternalBindingMap { async_wrap: AsyncWrapBinding; blob: BlobBinding; + 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; +}