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; +}