From b020b9661b7dc709ffb5c57fb1be618acd273a71 Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Wed, 7 Aug 2024 12:36:36 -0700 Subject: [PATCH 01/19] src: add WDAC integration (Windows) Add calls to Windows Defender Application Control to enforce integrity of .js, .json, .node files. --- doc/api/errors.md | 12 ++ lib/code_integrity.js | 28 ++++ lib/internal/errors.js | 4 + lib/internal/main/eval_string.js | 11 ++ lib/internal/modules/cjs/loader.js | 21 ++- node.gyp | 2 + src/node_binding.cc | 1 + src/node_code_integrity.cc | 208 +++++++++++++++++++++++++ src/node_code_integrity.h | 87 +++++++++++ src/node_external_reference.h | 1 + test/fixtures/code_integrity_test.js | 1 + test/fixtures/code_integrity_test.json | 1 + test/fixtures/code_integrity_test.node | 1 + test/parallel/test-code-integrity.js | 71 +++++++++ tsconfig.json | 1 + 15 files changed, 449 insertions(+), 1 deletion(-) create mode 100644 lib/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/parallel/test-code-integrity.js diff --git a/doc/api/errors.md b/doc/api/errors.md index f721ed221eab21..12f06721bd345a 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -789,6 +789,18 @@ changes: There was an attempt to use a `MessagePort` instance in a closed state, usually after `.close()` has been called. + + +### `ERR_CODE_INTEGRITY_BLOCKED` + +Feature has been disabled due to OS Code Integrity policy. + + + +### `ERR_CODE_INTEGRITY_VIOLATION` + +Javascript code intended to be executed was rejected by system code integrity policy. + ### `ERR_CONSOLE_WRITABLE_STREAM` diff --git a/lib/code_integrity.js b/lib/code_integrity.js new file mode 100644 index 00000000000000..378e1e353ba7e1 --- /dev/null +++ b/lib/code_integrity.js @@ -0,0 +1,28 @@ +'use strict'; + +let isCodeIntegrityEnforced; +let alreadyQueriedSystemCodeEnforcmentMode = false; + +const { + isFileTrustedBySystemCodeIntegrityPolicy, + isSystemEnforcingCodeIntegrity, +} = internalBinding('code_integrity'); + +function isAllowedToExecuteFile(filepath) { + if (!alreadyQueriedSystemCodeEnforcmentMode) { + isCodeIntegrityEnforced = isSystemEnforcingCodeIntegrity(); + alreadyQueriedSystemCodeEnforcmentMode = true; + } + + if (!isCodeIntegrityEnforced) { + return true; + } + + return isFileTrustedBySystemCodeIntegrityPolicy(filepath); +} + +module.exports = { + isAllowedToExecuteFile, + isFileTrustedBySystemCodeIntegrityPolicy, + isSystemEnforcingCodeIntegrity, +}; diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 47d4fbf677e5eb..8bb008a8115f49 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1144,6 +1144,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..14394dce988167 100644 --- a/lib/internal/main/eval_string.js +++ b/lib/internal/main/eval_string.js @@ -23,6 +23,17 @@ const { const { addBuiltinLibsToObject } = require('internal/modules/helpers'); const { getOptionValue } = require('internal/options'); +const { + codes: { + ERR_CODE_INTEGRITY_BLOCKED, + }, +} = require('internal/errors'); + +const ci = require('code_integrity'); +if (ci.isSystemEnforcingCodeIntegrity()) { + throw new ERR_CODE_INTEGRITY_BLOCKED('"eval"'); +} + prepareMainThreadExecution(); addBuiltinLibsToObject(globalThis, ''); markBootstrapComplete(); diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 39eea53e429681..9aea06f24cf600 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,8 @@ const onRequire = getLazy(() => tracingChannel('module.require')); const relativeResolveCache = { __proto__: null }; +const ci = require('code_integrity'); + let requireDepth = 0; let isPreloading = false; let statCache = null; @@ -1878,6 +1881,11 @@ function getRequireESMError(mod, pkg, content, filename) { * @param {string} filename The file path of the module */ Module._extensions['.js'] = function(module, filename) { + const isAllowedToExecute = ci.isAllowedToExecuteFile(filename); + if (!isAllowedToExecute) { + throw new ERR_CODE_INTEGRITY_VIOLATION(filename); + } + let format, pkg; if (StringPrototypeEndsWith(filename, '.cjs')) { format = 'commonjs'; @@ -1897,6 +1905,7 @@ Module._extensions['.js'] = function(module, filename) { throw err; } module._compile(source, filename, loadedFormat); + }; /** @@ -1905,6 +1914,12 @@ Module._extensions['.js'] = function(module, filename) { * @param {string} filename The file path of the module */ Module._extensions['.json'] = function(module, filename) { + + const isAllowedToExecute = ci.isAllowedToExecuteFile(filename); + if (!isAllowedToExecute) { + throw new ERR_CODE_INTEGRITY_VIOLATION(filename); + } + const { source: content } = loadSource(module, filename, 'json'); try { @@ -1921,7 +1936,11 @@ Module._extensions['.json'] = function(module, filename) { * @param {string} filename The file path of the module */ Module._extensions['.node'] = function(module, filename) { - // Be aware this doesn't use `content` + const isAllowedToExecute = ci.isAllowedToExecuteFile(filename); + if (!isAllowedToExecute) { + throw new ERR_CODE_INTEGRITY_VIOLATION(filename); + } + // Be aware this doesn't use `content` return process.dlopen(module, path.toNamespacedPath(filename)); }; diff --git a/node.gyp b/node.gyp index ba1f50d7541cca..dfb01ed3d62319 100644 --- a/node.gyp +++ b/node.gyp @@ -104,6 +104,7 @@ 'src/node_blob.cc', 'src/node_buffer.cc', 'src/node_builtins.cc', + 'src/node_code_integrity.cc', 'src/node_config.cc', 'src/node_constants.cc', 'src/node_contextify.cc', @@ -230,6 +231,7 @@ 'src/node_blob.h', 'src/node_buffer.h', 'src/node_builtins.h', + 'src/node_code_integrity.h', 'src/node_constants.h', 'src/node_context_data.h', 'src/node_contextify.h', diff --git a/src/node_binding.cc b/src/node_binding.cc index e93f3312cc6425..5a8ef378094e7e 100644 --- a/src/node_binding.cc +++ b/src/node_binding.cc @@ -43,6 +43,7 @@ V(buffer) \ V(builtins) \ V(cares_wrap) \ + V(code_integrity) \ V(config) \ V(constants) \ V(contextify) \ diff --git a/src/node_code_integrity.cc b/src/node_code_integrity.cc new file mode 100644 index 00000000000000..0201ca91a0de2f --- /dev/null +++ b/src/node_code_integrity.cc @@ -0,0 +1,208 @@ +#include "node_code_integrity.h" +#include "v8.h" +#include "node.h" +#include "env-inl.h" +#include "node_external_reference.h" + +namespace node { + +using v8::Boolean; +using v8::Context; +using v8::FunctionCallbackInfo; +using v8::Local; +using v8::Object; +using v8::Value; + +namespace codeintegrity { + +#ifdef _WIN32 +static bool isWldpInitialized = false; +static pfnWldpCanExecuteFile WldpCanExecuteFile; +static pfnWldpGetApplicationSettingBoolean WldpGetApplicationSettingBoolean; +static pfnWldpQuerySecurityPolicy WldpQuerySecurityPolicy; +static PCWSTR NODEJS = L"Node.js"; +static PCWSTR ENFORCE_CODE_INTEGRITY_SETTING_NAME = L"EnforceCodeIntegrity"; + +void InitWldp(Environment* env) { + + if (isWldpInitialized) + { + return; + } + + HMODULE wldp_module = LoadLibraryExA( + "wldp.dll", + nullptr, + LOAD_LIBRARY_SEARCH_SYSTEM32); + + if (wldp_module == nullptr) { + return env->ThrowError("Unable to load wldp.dll"); + } + + WldpCanExecuteFile = + (pfnWldpCanExecuteFile)GetProcAddress( + wldp_module, + "WldpCanExecuteFile"); + + WldpGetApplicationSettingBoolean = + (pfnWldpGetApplicationSettingBoolean)GetProcAddress( + wldp_module, + "WldpGetApplicationSettingBoolean"); + + WldpQuerySecurityPolicy = + (pfnWldpQuerySecurityPolicy)GetProcAddress( + wldp_module, + "WldpQuerySecurityPolicy"); + + isWldpInitialized = true; +} + +static void IsFileTrustedBySystemCodeIntegrityPolicy( + const FunctionCallbackInfo& args) { + CHECK_EQ(args.Length(), 1); + CHECK(args[0]->IsString()); + + Environment* env = Environment::GetCurrent(args); + if (!isWldpInitialized) { + InitWldp(env); + } + + BufferValue path(env->isolate(), args[0]); + if (*path == nullptr) { + return env->ThrowError("path cannot be empty"); + } + + HANDLE hFile = CreateFileA( + *path, + GENERIC_READ, + FILE_SHARE_READ, + nullptr, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + nullptr); + + if (hFile == INVALID_HANDLE_VALUE || hFile == nullptr) { + return env->ThrowError("Unable to open file"); + } + + const GUID wldp_host_other = WLDP_HOST_OTHER; + WLDP_EXECUTION_POLICY result; + HRESULT hr = WldpCanExecuteFile( + wldp_host_other, + WLDP_EXECUTION_EVALUATION_OPTION_NONE, + hFile, + NODEJS, + &result); + CloseHandle(hFile); + + if (FAILED(hr)) { + return env->ThrowError("WldpCanExecuteFile failed"); + } + + bool isFileTrusted = (result == WLDP_EXECUTION_POLICY_ALLOWED); + args.GetReturnValue().Set(isFileTrusted); +} + +static void IsSystemEnforcingCodeIntegrity( + const FunctionCallbackInfo& args) { + CHECK_EQ(args.Length(), 0); + + Environment* env = Environment::GetCurrent(args); + + if (!isWldpInitialized) { + InitWldp(env); + } + + if (WldpGetApplicationSettingBoolean != nullptr) + { + BOOL ret; + HRESULT hr = WldpGetApplicationSettingBoolean( + NODEJS, + ENFORCE_CODE_INTEGRITY_SETTING_NAME, + &ret); + + if (SUCCEEDED(hr)) { + args.GetReturnValue().Set( + Boolean::New(env->isolate(), ret)); + 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().Set(Boolean::New(env->isolate(), false)); + 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 (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 ret = 0; + HRESULT hr = WldpQuerySecurityPolicy( + &providerName, + &keyName, + &valueName, + &valueType, + &ret, + &valueSize); + if (FAILED(hr)) { + args.GetReturnValue().Set(Boolean::New(env->isolate(), false)); + return; + } + + args.GetReturnValue().Set( + Boolean::New(env->isolate(), static_cast(ret))); + } +} +#endif // _WIN32 + +#ifndef _WIN32 +static void IsFileTrustedBySystemCodeIntegrityPolicy( + const FunctionCallbackInfo& args) { + args.GetReturnValue().Set(true); +} + +static void IsSystemEnforcingCodeIntegrity( + const FunctionCallbackInfo& args) { + args.GetReturnValue().Set(false); +} +#endif // ifndef _WIN32 + +void Initialize(Local target, + Local unused, + Local context, + void* priv) { + SetMethod( + context, + target, + "isFileTrustedBySystemCodeIntegrityPolicy", + IsFileTrustedBySystemCodeIntegrityPolicy); + + SetMethod( + context, + target, + "isSystemEnforcingCodeIntegrity", + IsSystemEnforcingCodeIntegrity); +} + +void RegisterExternalReferences(ExternalReferenceRegistry* registry) { + //BindingData::RegisterExternalReferences(registry); + + registry->Register(IsFileTrustedBySystemCodeIntegrityPolicy); + registry->Register(IsSystemEnforcingCodeIntegrity); +} + +} // namespace codeintegrity +} // namespace node +NODE_BINDING_CONTEXT_AWARE_INTERNAL(code_integrity, + node::codeintegrity::Initialize) +NODE_BINDING_EXTERNAL_REFERENCE(code_integrity, + node::codeintegrity::RegisterExternalReferences) \ No newline at end of file diff --git a/src/node_code_integrity.h b/src/node_code_integrity.h new file mode 100644 index 00000000000000..1a79ac0736d136 --- /dev/null +++ b/src/node_code_integrity.h @@ -0,0 +1,87 @@ +#ifndef SRC_NODE_CODE_INTEGRITY_H_ +#define SRC_NODE_CODE_INTEGRITY_H_ + +#ifdef _WIN32 + +#include + +// {0xb5367df1,0xcbac,0x11cf,{0x95,0xca,0x00,0x80,0x5f,0x48,0xa1,0x92}} +#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 // _WIN32 +#endif // SRC_NODE_CODE_INTEGRITY_H_ \ No newline at end of file diff --git a/src/node_external_reference.h b/src/node_external_reference.h index 3976a0e9fb23d4..54abd41f1179a8 100644 --- a/src/node_external_reference.h +++ b/src/node_external_reference.h @@ -151,6 +151,7 @@ class ExternalReferenceRegistry { V(builtins) \ V(cares_wrap) \ V(config) \ + V(code_integrity) \ V(contextify) \ V(credentials) \ V(encoding_binding) \ diff --git a/test/fixtures/code_integrity_test.js b/test/fixtures/code_integrity_test.js new file mode 100644 index 00000000000000..ec630ce6953fbd --- /dev/null +++ b/test/fixtures/code_integrity_test.js @@ -0,0 +1 @@ +1 + 1; \ No newline at end of file diff --git a/test/fixtures/code_integrity_test.json b/test/fixtures/code_integrity_test.json new file mode 100644 index 00000000000000..9e26dfeeb6e641 --- /dev/null +++ b/test/fixtures/code_integrity_test.json @@ -0,0 +1 @@ +{} \ No newline at end of file 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/parallel/test-code-integrity.js b/test/parallel/test-code-integrity.js new file mode 100644 index 00000000000000..19eab6b21f5bd0 --- /dev/null +++ b/test/parallel/test-code-integrity.js @@ -0,0 +1,71 @@ +'use strict'; + +// Flags: --expose-internals + +require('../common'); +const assert = require('node:assert'); +const { describe, it } = require('node:test'); +const ci = require('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_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_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_INTEGRITY_VIOLATION', + }, + ); + } + ); +}); diff --git a/tsconfig.json b/tsconfig.json index 3f5a3067ced063..9d280df245cf60 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -34,6 +34,7 @@ "buffer": ["./lib/buffer.js"], "child_process": ["./lib/child_process.js"], "cluster": ["./lib/cluster.js"], + "codeintegrity": ["./lib/codeintegrity.js"], "console": ["./lib/console.js"], "constants": ["./lib/constants.js"], "crypto": ["./lib/crypto.js"], From 9201ba77aefa73ea1893aefd665c74bcd27cd9e2 Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Tue, 13 Aug 2024 17:29:46 -0700 Subject: [PATCH 02/19] appease linter --- doc/api/errors.md | 2 +- test/parallel/test-code-integrity.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/api/errors.md b/doc/api/errors.md index 12f06721bd345a..c8d4f5260ffb65 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -799,7 +799,7 @@ Feature has been disabled due to OS Code Integrity policy. ### `ERR_CODE_INTEGRITY_VIOLATION` -Javascript code intended to be executed was rejected by system code integrity policy. +JavaScript code intended to be executed was rejected by system code integrity policy. diff --git a/test/parallel/test-code-integrity.js b/test/parallel/test-code-integrity.js index 19eab6b21f5bd0..0af079e2652064 100644 --- a/test/parallel/test-code-integrity.js +++ b/test/parallel/test-code-integrity.js @@ -50,7 +50,7 @@ describe('cjs loader code integrity integration tests', () => { t.mock.method(ci, ci.isAllowedToExecuteFile.name, () => { return true; }); assert.ok( - require('../fixtures/code_integrity_test.json') + require('../fixtures/code_integrity_test.json') ); } ); From 68a2d02b4716ddc2f6ddc03e2a47e673db58e35a Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Mon, 19 Aug 2024 10:44:16 -0700 Subject: [PATCH 03/19] use LF instead of CRLF --- lib/code_integrity.js | 56 ++-- src/node_code_integrity.cc | 414 +++++++++++++-------------- src/node_code_integrity.h | 172 +++++------ test/parallel/test-code-integrity.js | 142 ++++----- 4 files changed, 392 insertions(+), 392 deletions(-) diff --git a/lib/code_integrity.js b/lib/code_integrity.js index 378e1e353ba7e1..715a4e3d0163ea 100644 --- a/lib/code_integrity.js +++ b/lib/code_integrity.js @@ -1,28 +1,28 @@ -'use strict'; - -let isCodeIntegrityEnforced; -let alreadyQueriedSystemCodeEnforcmentMode = false; - -const { - isFileTrustedBySystemCodeIntegrityPolicy, - isSystemEnforcingCodeIntegrity, -} = internalBinding('code_integrity'); - -function isAllowedToExecuteFile(filepath) { - if (!alreadyQueriedSystemCodeEnforcmentMode) { - isCodeIntegrityEnforced = isSystemEnforcingCodeIntegrity(); - alreadyQueriedSystemCodeEnforcmentMode = true; - } - - if (!isCodeIntegrityEnforced) { - return true; - } - - return isFileTrustedBySystemCodeIntegrityPolicy(filepath); -} - -module.exports = { - isAllowedToExecuteFile, - isFileTrustedBySystemCodeIntegrityPolicy, - isSystemEnforcingCodeIntegrity, -}; +'use strict'; + +let isCodeIntegrityEnforced; +let alreadyQueriedSystemCodeEnforcmentMode = false; + +const { + isFileTrustedBySystemCodeIntegrityPolicy, + isSystemEnforcingCodeIntegrity, +} = internalBinding('code_integrity'); + +function isAllowedToExecuteFile(filepath) { + if (!alreadyQueriedSystemCodeEnforcmentMode) { + isCodeIntegrityEnforced = isSystemEnforcingCodeIntegrity(); + alreadyQueriedSystemCodeEnforcmentMode = true; + } + + if (!isCodeIntegrityEnforced) { + return true; + } + + return isFileTrustedBySystemCodeIntegrityPolicy(filepath); +} + +module.exports = { + isAllowedToExecuteFile, + isFileTrustedBySystemCodeIntegrityPolicy, + isSystemEnforcingCodeIntegrity, +}; diff --git a/src/node_code_integrity.cc b/src/node_code_integrity.cc index 0201ca91a0de2f..3ac671c1c82124 100644 --- a/src/node_code_integrity.cc +++ b/src/node_code_integrity.cc @@ -1,208 +1,208 @@ -#include "node_code_integrity.h" -#include "v8.h" -#include "node.h" -#include "env-inl.h" -#include "node_external_reference.h" - -namespace node { - -using v8::Boolean; -using v8::Context; -using v8::FunctionCallbackInfo; -using v8::Local; -using v8::Object; -using v8::Value; - -namespace codeintegrity { - -#ifdef _WIN32 -static bool isWldpInitialized = false; -static pfnWldpCanExecuteFile WldpCanExecuteFile; -static pfnWldpGetApplicationSettingBoolean WldpGetApplicationSettingBoolean; -static pfnWldpQuerySecurityPolicy WldpQuerySecurityPolicy; -static PCWSTR NODEJS = L"Node.js"; -static PCWSTR ENFORCE_CODE_INTEGRITY_SETTING_NAME = L"EnforceCodeIntegrity"; - -void InitWldp(Environment* env) { - - if (isWldpInitialized) - { - return; - } - - HMODULE wldp_module = LoadLibraryExA( - "wldp.dll", - nullptr, - LOAD_LIBRARY_SEARCH_SYSTEM32); - - if (wldp_module == nullptr) { - return env->ThrowError("Unable to load wldp.dll"); - } - - WldpCanExecuteFile = - (pfnWldpCanExecuteFile)GetProcAddress( - wldp_module, - "WldpCanExecuteFile"); - - WldpGetApplicationSettingBoolean = - (pfnWldpGetApplicationSettingBoolean)GetProcAddress( - wldp_module, - "WldpGetApplicationSettingBoolean"); - - WldpQuerySecurityPolicy = - (pfnWldpQuerySecurityPolicy)GetProcAddress( - wldp_module, - "WldpQuerySecurityPolicy"); - - isWldpInitialized = true; -} - -static void IsFileTrustedBySystemCodeIntegrityPolicy( - const FunctionCallbackInfo& args) { - CHECK_EQ(args.Length(), 1); - CHECK(args[0]->IsString()); - - Environment* env = Environment::GetCurrent(args); - if (!isWldpInitialized) { - InitWldp(env); - } - - BufferValue path(env->isolate(), args[0]); - if (*path == nullptr) { - return env->ThrowError("path cannot be empty"); - } - - HANDLE hFile = CreateFileA( - *path, - GENERIC_READ, - FILE_SHARE_READ, - nullptr, - OPEN_EXISTING, - FILE_ATTRIBUTE_NORMAL, - nullptr); - - if (hFile == INVALID_HANDLE_VALUE || hFile == nullptr) { - return env->ThrowError("Unable to open file"); - } - - const GUID wldp_host_other = WLDP_HOST_OTHER; - WLDP_EXECUTION_POLICY result; - HRESULT hr = WldpCanExecuteFile( - wldp_host_other, - WLDP_EXECUTION_EVALUATION_OPTION_NONE, - hFile, - NODEJS, - &result); - CloseHandle(hFile); - - if (FAILED(hr)) { - return env->ThrowError("WldpCanExecuteFile failed"); - } - - bool isFileTrusted = (result == WLDP_EXECUTION_POLICY_ALLOWED); - args.GetReturnValue().Set(isFileTrusted); -} - -static void IsSystemEnforcingCodeIntegrity( - const FunctionCallbackInfo& args) { - CHECK_EQ(args.Length(), 0); - - Environment* env = Environment::GetCurrent(args); - - if (!isWldpInitialized) { - InitWldp(env); - } - - if (WldpGetApplicationSettingBoolean != nullptr) - { - BOOL ret; - HRESULT hr = WldpGetApplicationSettingBoolean( - NODEJS, - ENFORCE_CODE_INTEGRITY_SETTING_NAME, - &ret); - - if (SUCCEEDED(hr)) { - args.GetReturnValue().Set( - Boolean::New(env->isolate(), ret)); - 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().Set(Boolean::New(env->isolate(), false)); - 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 (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 ret = 0; - HRESULT hr = WldpQuerySecurityPolicy( - &providerName, - &keyName, - &valueName, - &valueType, - &ret, - &valueSize); - if (FAILED(hr)) { - args.GetReturnValue().Set(Boolean::New(env->isolate(), false)); - return; - } - - args.GetReturnValue().Set( - Boolean::New(env->isolate(), static_cast(ret))); - } -} -#endif // _WIN32 - -#ifndef _WIN32 -static void IsFileTrustedBySystemCodeIntegrityPolicy( - const FunctionCallbackInfo& args) { - args.GetReturnValue().Set(true); -} - -static void IsSystemEnforcingCodeIntegrity( - const FunctionCallbackInfo& args) { - args.GetReturnValue().Set(false); -} -#endif // ifndef _WIN32 - -void Initialize(Local target, - Local unused, - Local context, - void* priv) { - SetMethod( - context, - target, - "isFileTrustedBySystemCodeIntegrityPolicy", - IsFileTrustedBySystemCodeIntegrityPolicy); - - SetMethod( - context, - target, - "isSystemEnforcingCodeIntegrity", - IsSystemEnforcingCodeIntegrity); -} - -void RegisterExternalReferences(ExternalReferenceRegistry* registry) { - //BindingData::RegisterExternalReferences(registry); - - registry->Register(IsFileTrustedBySystemCodeIntegrityPolicy); - registry->Register(IsSystemEnforcingCodeIntegrity); -} - -} // namespace codeintegrity -} // namespace node -NODE_BINDING_CONTEXT_AWARE_INTERNAL(code_integrity, - node::codeintegrity::Initialize) -NODE_BINDING_EXTERNAL_REFERENCE(code_integrity, +#include "node_code_integrity.h" +#include "v8.h" +#include "node.h" +#include "env-inl.h" +#include "node_external_reference.h" + +namespace node { + +using v8::Boolean; +using v8::Context; +using v8::FunctionCallbackInfo; +using v8::Local; +using v8::Object; +using v8::Value; + +namespace codeintegrity { + +#ifdef _WIN32 +static bool isWldpInitialized = false; +static pfnWldpCanExecuteFile WldpCanExecuteFile; +static pfnWldpGetApplicationSettingBoolean WldpGetApplicationSettingBoolean; +static pfnWldpQuerySecurityPolicy WldpQuerySecurityPolicy; +static PCWSTR NODEJS = L"Node.js"; +static PCWSTR ENFORCE_CODE_INTEGRITY_SETTING_NAME = L"EnforceCodeIntegrity"; + +void InitWldp(Environment* env) { + + if (isWldpInitialized) + { + return; + } + + HMODULE wldp_module = LoadLibraryExA( + "wldp.dll", + nullptr, + LOAD_LIBRARY_SEARCH_SYSTEM32); + + if (wldp_module == nullptr) { + return env->ThrowError("Unable to load wldp.dll"); + } + + WldpCanExecuteFile = + (pfnWldpCanExecuteFile)GetProcAddress( + wldp_module, + "WldpCanExecuteFile"); + + WldpGetApplicationSettingBoolean = + (pfnWldpGetApplicationSettingBoolean)GetProcAddress( + wldp_module, + "WldpGetApplicationSettingBoolean"); + + WldpQuerySecurityPolicy = + (pfnWldpQuerySecurityPolicy)GetProcAddress( + wldp_module, + "WldpQuerySecurityPolicy"); + + isWldpInitialized = true; +} + +static void IsFileTrustedBySystemCodeIntegrityPolicy( + const FunctionCallbackInfo& args) { + CHECK_EQ(args.Length(), 1); + CHECK(args[0]->IsString()); + + Environment* env = Environment::GetCurrent(args); + if (!isWldpInitialized) { + InitWldp(env); + } + + BufferValue path(env->isolate(), args[0]); + if (*path == nullptr) { + return env->ThrowError("path cannot be empty"); + } + + HANDLE hFile = CreateFileA( + *path, + GENERIC_READ, + FILE_SHARE_READ, + nullptr, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + nullptr); + + if (hFile == INVALID_HANDLE_VALUE || hFile == nullptr) { + return env->ThrowError("Unable to open file"); + } + + const GUID wldp_host_other = WLDP_HOST_OTHER; + WLDP_EXECUTION_POLICY result; + HRESULT hr = WldpCanExecuteFile( + wldp_host_other, + WLDP_EXECUTION_EVALUATION_OPTION_NONE, + hFile, + NODEJS, + &result); + CloseHandle(hFile); + + if (FAILED(hr)) { + return env->ThrowError("WldpCanExecuteFile failed"); + } + + bool isFileTrusted = (result == WLDP_EXECUTION_POLICY_ALLOWED); + args.GetReturnValue().Set(isFileTrusted); +} + +static void IsSystemEnforcingCodeIntegrity( + const FunctionCallbackInfo& args) { + CHECK_EQ(args.Length(), 0); + + Environment* env = Environment::GetCurrent(args); + + if (!isWldpInitialized) { + InitWldp(env); + } + + if (WldpGetApplicationSettingBoolean != nullptr) + { + BOOL ret; + HRESULT hr = WldpGetApplicationSettingBoolean( + NODEJS, + ENFORCE_CODE_INTEGRITY_SETTING_NAME, + &ret); + + if (SUCCEEDED(hr)) { + args.GetReturnValue().Set( + Boolean::New(env->isolate(), ret)); + 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().Set(Boolean::New(env->isolate(), false)); + 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 (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 ret = 0; + HRESULT hr = WldpQuerySecurityPolicy( + &providerName, + &keyName, + &valueName, + &valueType, + &ret, + &valueSize); + if (FAILED(hr)) { + args.GetReturnValue().Set(Boolean::New(env->isolate(), false)); + return; + } + + args.GetReturnValue().Set( + Boolean::New(env->isolate(), static_cast(ret))); + } +} +#endif // _WIN32 + +#ifndef _WIN32 +static void IsFileTrustedBySystemCodeIntegrityPolicy( + const FunctionCallbackInfo& args) { + args.GetReturnValue().Set(true); +} + +static void IsSystemEnforcingCodeIntegrity( + const FunctionCallbackInfo& args) { + args.GetReturnValue().Set(false); +} +#endif // ifndef _WIN32 + +void Initialize(Local target, + Local unused, + Local context, + void* priv) { + SetMethod( + context, + target, + "isFileTrustedBySystemCodeIntegrityPolicy", + IsFileTrustedBySystemCodeIntegrityPolicy); + + SetMethod( + context, + target, + "isSystemEnforcingCodeIntegrity", + IsSystemEnforcingCodeIntegrity); +} + +void RegisterExternalReferences(ExternalReferenceRegistry* registry) { + //BindingData::RegisterExternalReferences(registry); + + registry->Register(IsFileTrustedBySystemCodeIntegrityPolicy); + registry->Register(IsSystemEnforcingCodeIntegrity); +} + +} // namespace codeintegrity +} // namespace node +NODE_BINDING_CONTEXT_AWARE_INTERNAL(code_integrity, + node::codeintegrity::Initialize) +NODE_BINDING_EXTERNAL_REFERENCE(code_integrity, node::codeintegrity::RegisterExternalReferences) \ No newline at end of file diff --git a/src/node_code_integrity.h b/src/node_code_integrity.h index 1a79ac0736d136..75802ffeaaed9a 100644 --- a/src/node_code_integrity.h +++ b/src/node_code_integrity.h @@ -1,87 +1,87 @@ -#ifndef SRC_NODE_CODE_INTEGRITY_H_ -#define SRC_NODE_CODE_INTEGRITY_H_ - -#ifdef _WIN32 - -#include - -// {0xb5367df1,0xcbac,0x11cf,{0x95,0xca,0x00,0x80,0x5f,0x48,0xa1,0x92}} -#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 // _WIN32 +#ifndef SRC_NODE_CODE_INTEGRITY_H_ +#define SRC_NODE_CODE_INTEGRITY_H_ + +#ifdef _WIN32 + +#include + +// {0xb5367df1,0xcbac,0x11cf,{0x95,0xca,0x00,0x80,0x5f,0x48,0xa1,0x92}} +#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 // _WIN32 #endif // SRC_NODE_CODE_INTEGRITY_H_ \ No newline at end of file diff --git a/test/parallel/test-code-integrity.js b/test/parallel/test-code-integrity.js index 0af079e2652064..ed1fd1cd05c665 100644 --- a/test/parallel/test-code-integrity.js +++ b/test/parallel/test-code-integrity.js @@ -1,71 +1,71 @@ -'use strict'; - -// Flags: --expose-internals - -require('../common'); -const assert = require('node:assert'); -const { describe, it } = require('node:test'); -const ci = require('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_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_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_INTEGRITY_VIOLATION', - }, - ); - } - ); -}); +'use strict'; + +// Flags: --expose-internals + +require('../common'); +const assert = require('node:assert'); +const { describe, it } = require('node:test'); +const ci = require('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_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_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_INTEGRITY_VIOLATION', + }, + ); + } + ); +}); From acedaeacdf66062af3879c882820e153e85c7160 Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Tue, 20 Aug 2024 13:24:51 -0700 Subject: [PATCH 04/19] fix expected exception --- test/parallel/test-code-integrity.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/parallel/test-code-integrity.js b/test/parallel/test-code-integrity.js index ed1fd1cd05c665..bd3476e0bec462 100644 --- a/test/parallel/test-code-integrity.js +++ b/test/parallel/test-code-integrity.js @@ -17,7 +17,7 @@ describe('cjs loader code integrity integration tests', () => { require('../fixtures/code_integrity_test.js'); }, { - code: 'ERR_INTEGRITY_VIOLATION', + code: 'ERR_CODE_INTEGRITY_VIOLATION', }, ); } @@ -40,7 +40,7 @@ describe('cjs loader code integrity integration tests', () => { require('../fixtures/code_integrity_test.json'); }, { - code: 'ERR_INTEGRITY_VIOLATION', + code: 'ERR_CODE_INTEGRITY_VIOLATION', }, ); } @@ -63,7 +63,7 @@ describe('cjs loader code integrity integration tests', () => { require('../fixtures/code_integrity_test.node'); }, { - code: 'ERR_INTEGRITY_VIOLATION', + code: 'ERR_CODE_INTEGRITY_VIOLATION', }, ); } From d3a82a578052f1a813ec477251e7c1cf4dd2faf6 Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Tue, 20 Aug 2024 13:25:14 -0700 Subject: [PATCH 05/19] move code_integrity to internal module, fix formatting --- lib/{ => internal}/code_integrity.js | 0 lib/internal/main/eval_string.js | 2 +- lib/internal/modules/cjs/loader.js | 2 +- src/node_code_integrity.cc | 29 +++++++++++++--------------- src/node_code_integrity.h | 8 ++++---- 5 files changed, 19 insertions(+), 22 deletions(-) rename lib/{ => internal}/code_integrity.js (100%) diff --git a/lib/code_integrity.js b/lib/internal/code_integrity.js similarity index 100% rename from lib/code_integrity.js rename to lib/internal/code_integrity.js diff --git a/lib/internal/main/eval_string.js b/lib/internal/main/eval_string.js index 14394dce988167..2d492682fffb7e 100644 --- a/lib/internal/main/eval_string.js +++ b/lib/internal/main/eval_string.js @@ -29,7 +29,7 @@ const { }, } = require('internal/errors'); -const ci = require('code_integrity'); +const ci = require('internal/code_integrity'); if (ci.isSystemEnforcingCodeIntegrity()) { throw new ERR_CODE_INTEGRITY_BLOCKED('"eval"'); } diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 9aea06f24cf600..6365c979388be1 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -217,7 +217,7 @@ const onRequire = getLazy(() => tracingChannel('module.require')); const relativeResolveCache = { __proto__: null }; -const ci = require('code_integrity'); +const ci = require('internal/code_integrity'); let requireDepth = 0; let isPreloading = false; diff --git a/src/node_code_integrity.cc b/src/node_code_integrity.cc index 3ac671c1c82124..86662bac6aedde 100644 --- a/src/node_code_integrity.cc +++ b/src/node_code_integrity.cc @@ -24,9 +24,7 @@ static PCWSTR NODEJS = L"Node.js"; static PCWSTR ENFORCE_CODE_INTEGRITY_SETTING_NAME = L"EnforceCodeIntegrity"; void InitWldp(Environment* env) { - - if (isWldpInitialized) - { + if (isWldpInitialized) { return; } @@ -53,7 +51,7 @@ void InitWldp(Environment* env) { (pfnWldpQuerySecurityPolicy)GetProcAddress( wldp_module, "WldpQuerySecurityPolicy"); - + isWldpInitialized = true; } @@ -113,8 +111,7 @@ static void IsSystemEnforcingCodeIntegrity( InitWldp(env); } - if (WldpGetApplicationSettingBoolean != nullptr) - { + if (WldpGetApplicationSettingBoolean != nullptr) { BOOL ret; HRESULT hr = WldpGetApplicationSettingBoolean( NODEJS, @@ -125,14 +122,15 @@ static void IsSystemEnforcingCodeIntegrity( args.GetReturnValue().Set( Boolean::New(env->isolate(), ret)); 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 + } 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().Set(Boolean::New(env->isolate(), false)); 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 @@ -162,7 +160,7 @@ static void IsSystemEnforcingCodeIntegrity( Boolean::New(env->isolate(), static_cast(ret))); } } -#endif // _WIN32 +#endif // _WIN32 #ifndef _WIN32 static void IsFileTrustedBySystemCodeIntegrityPolicy( @@ -174,7 +172,7 @@ static void IsSystemEnforcingCodeIntegrity( const FunctionCallbackInfo& args) { args.GetReturnValue().Set(false); } -#endif // ifndef _WIN32 +#endif // ifndef _WIN32 void Initialize(Local target, Local unused, @@ -194,15 +192,14 @@ void Initialize(Local target, } void RegisterExternalReferences(ExternalReferenceRegistry* registry) { - //BindingData::RegisterExternalReferences(registry); - registry->Register(IsFileTrustedBySystemCodeIntegrityPolicy); registry->Register(IsSystemEnforcingCodeIntegrity); } } // namespace codeintegrity } // namespace node + NODE_BINDING_CONTEXT_AWARE_INTERNAL(code_integrity, node::codeintegrity::Initialize) NODE_BINDING_EXTERNAL_REFERENCE(code_integrity, - node::codeintegrity::RegisterExternalReferences) \ No newline at end of file + node::codeintegrity::RegisterExternalReferences) diff --git a/src/node_code_integrity.h b/src/node_code_integrity.h index 75802ffeaaed9a..d062ff6575e96a 100644 --- a/src/node_code_integrity.h +++ b/src/node_code_integrity.h @@ -34,10 +34,10 @@ typedef HRESULT(WINAPI* pfnWldpCanExecuteFile)( typedef HRESULT(WINAPI* pfnWldpCanExecuteBuffer)( _In_ REFGUID host, _In_ WLDP_EXECUTION_EVALUATION_OPTIONS options, - _In_reads_(bufferSize) const BYTE *buffer, + _In_reads_(bufferSize) const BYTE* buffer, _In_ ULONG bufferSize, _In_opt_ PCWSTR auditInfo, - _Out_ WLDP_EXECUTION_POLICY *result); + _Out_ WLDP_EXECUTION_POLICY* result); typedef HRESULT(WINAPI* pfnWldpGetApplicationSettingBoolean)( _In_ PCWSTR id, @@ -77,11 +77,11 @@ typedef HRESULT(WINAPI* pfnWldpQuerySecurityPolicy)( const WCHAR _var ## _buffer[] = _string; \ const UNICODE_STRING _var = \ { sizeof(_string) - sizeof(WCHAR), sizeof(_string), (PWCH) _var ## _buffer } -#endif +#endif #ifndef E_NOTFOUND #define E_NOTFOUND 0x80070490 #endif #endif // _WIN32 -#endif // SRC_NODE_CODE_INTEGRITY_H_ \ No newline at end of file +#endif // SRC_NODE_CODE_INTEGRITY_H_ From 274c1e2577ad66d08398225853f7c96e32ec1fce Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Wed, 9 Oct 2024 10:28:22 -0700 Subject: [PATCH 06/19] add DisableInteractiveMode code integrity setting --- lib/internal/code_integrity.js | 13 ++++++ lib/internal/main/eval_string.js | 2 +- src/node_code_integrity.cc | 74 ++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 1 deletion(-) diff --git a/lib/internal/code_integrity.js b/lib/internal/code_integrity.js index 715a4e3d0163ea..e444e04896d872 100644 --- a/lib/internal/code_integrity.js +++ b/lib/internal/code_integrity.js @@ -1,16 +1,28 @@ 'use strict'; +const { emitWarning } = require('internal/process/warning'); + let isCodeIntegrityEnforced; let alreadyQueriedSystemCodeEnforcmentMode = false; const { isFileTrustedBySystemCodeIntegrityPolicy, + isInteractiveModeDisabled, isSystemEnforcingCodeIntegrity, } = internalBinding('code_integrity'); 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; } @@ -24,5 +36,6 @@ function isAllowedToExecuteFile(filepath) { module.exports = { isAllowedToExecuteFile, isFileTrustedBySystemCodeIntegrityPolicy, + isInteractiveModeDisabled, isSystemEnforcingCodeIntegrity, }; diff --git a/lib/internal/main/eval_string.js b/lib/internal/main/eval_string.js index 2d492682fffb7e..cd8f6ba65b8b28 100644 --- a/lib/internal/main/eval_string.js +++ b/lib/internal/main/eval_string.js @@ -30,7 +30,7 @@ const { } = require('internal/errors'); const ci = require('internal/code_integrity'); -if (ci.isSystemEnforcingCodeIntegrity()) { +if (ci.isInteractiveModeDisabled()) { throw new ERR_CODE_INTEGRITY_BLOCKED('"eval"'); } diff --git a/src/node_code_integrity.cc b/src/node_code_integrity.cc index 86662bac6aedde..d99361a92f5fa7 100644 --- a/src/node_code_integrity.cc +++ b/src/node_code_integrity.cc @@ -22,6 +22,8 @@ static pfnWldpGetApplicationSettingBoolean WldpGetApplicationSettingBoolean; static pfnWldpQuerySecurityPolicy WldpQuerySecurityPolicy; static PCWSTR NODEJS = L"Node.js"; static PCWSTR ENFORCE_CODE_INTEGRITY_SETTING_NAME = L"EnforceCodeIntegrity"; +static PCWSTR DISABLE_INTERPRETIVE_MODE_SETTING_NAME = + L"DisableInteractiveMode"; void InitWldp(Environment* env) { if (isWldpInitialized) { @@ -101,6 +103,66 @@ static void IsFileTrustedBySystemCodeIntegrityPolicy( args.GetReturnValue().Set(isFileTrusted); } +static void IsInteractiveModeDisabled( + const FunctionCallbackInfo& args) { + CHECK_EQ(args.Length(), 0); + + Environment* env = Environment::GetCurrent(args); + + if (!isWldpInitialized) { + InitWldp(env); + } + + if (WldpGetApplicationSettingBoolean != nullptr) { + BOOL ret; + HRESULT hr = WldpGetApplicationSettingBoolean( + NODEJS, + DISABLE_INTERPRETIVE_MODE_SETTING_NAME, + &ret); + + if (SUCCEEDED(hr)) { + args.GetReturnValue().Set( + Boolean::New(env->isolate(), ret)); + 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().Set(Boolean::New(env->isolate(), false)); + 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 (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 ret = 0; + HRESULT hr = WldpQuerySecurityPolicy( + &providerName, + &keyName, + &valueName, + &valueType, + &ret, + &valueSize); + if (FAILED(hr)) { + args.GetReturnValue().Set(Boolean::New(env->isolate(), false)); + return; + } + + args.GetReturnValue().Set( + Boolean::New(env->isolate(), static_cast(ret))); + } +} + static void IsSystemEnforcingCodeIntegrity( const FunctionCallbackInfo& args) { CHECK_EQ(args.Length(), 0); @@ -168,6 +230,11 @@ static void IsFileTrustedBySystemCodeIntegrityPolicy( args.GetReturnValue().Set(true); } +static void IsInterpretiveModeDisabled( + const FunctionCallbackInfo& args) { + args.GetReturnValue().Set(false); +} + static void IsSystemEnforcingCodeIntegrity( const FunctionCallbackInfo& args) { args.GetReturnValue().Set(false); @@ -184,6 +251,12 @@ void Initialize(Local target, "isFileTrustedBySystemCodeIntegrityPolicy", IsFileTrustedBySystemCodeIntegrityPolicy); + SetMethod( + context, + target, + "isInteractiveModeDisabled", + IsInteractiveModeDisabled); + SetMethod( context, target, @@ -193,6 +266,7 @@ void Initialize(Local target, void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(IsFileTrustedBySystemCodeIntegrityPolicy); + registry->Register(IsInteractiveModeDisabled); registry->Register(IsSystemEnforcingCodeIntegrity); } From b76a2fa0785610bafa7194f03b4e7b993ffbac1b Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Wed, 9 Oct 2024 10:28:51 -0700 Subject: [PATCH 07/19] add more code integrity docs --- doc/api/code_integrity.md | 116 ++++++++++++++++++++++++++++++++++++++ doc/api/wdac-manifest.xml | 5 ++ 2 files changed, 121 insertions(+) create mode 100644 doc/api/code_integrity.md create mode 100644 doc/api/wdac-manifest.xml diff --git a/doc/api/code_integrity.md b/doc/api/code_integrity.md new file mode 100644 index 00000000000000..83c7bccb1e8150 --- /dev/null +++ b/doc/api/code_integrity.md @@ -0,0 +1,116 @@ +# Code Integrity + +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 + +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. + +### Windows Code Integrity and Application Developers + +Application developers are responsible for generating and +distributing the signature information for their application. +Application developers are also expected to design their application +in robust ways to avoid unintended code execution. + +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 for the final version of the files that you intend to ship +(i.e. after minifying). + +### 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 can be found [here](https://learn.microsoft.com/en-us/windows/security/application-security/application-control/windows-defender-application-control/). + +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 + + + +``` + +### Windows Code Integrity and End Users + +Depending on + +## 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. + +## Code Integrity on MacOS + +Code integrity on MacOS is not yet implemented. Currently, there is no +timeline for implementation. + +[Security Policy]: https://github.com/nodejs/node/blob/main/SECURITY.md diff --git a/doc/api/wdac-manifest.xml b/doc/api/wdac-manifest.xml new file mode 100644 index 00000000000000..8bab5cfb16b165 --- /dev/null +++ b/doc/api/wdac-manifest.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file From d9ddfc96d9e4efb6aeb78b097ce07fad936851f4 Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Mon, 11 Nov 2024 09:23:43 -0800 Subject: [PATCH 08/19] clarify docs, add active development status --- doc/api/code_integrity.md | 11 ++++++----- doc/api/errors.md | 2 ++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/doc/api/code_integrity.md b/doc/api/code_integrity.md index 83c7bccb1e8150..f8a581cf508027 100644 --- a/doc/api/code_integrity.md +++ b/doc/api/code_integrity.md @@ -1,5 +1,9 @@ # Code Integrity + + +> Stability: 1.1 - Active development + 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. @@ -99,14 +103,11 @@ On older Windows versions, use the `Settings` section of your WDAC Policy. ``` -### Windows Code Integrity and End Users - -Depending on - ## 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. +be made once the necessary APIs on Linux have been upstreamed. More information +can be found here: https://github.com/nodejs/security-wg/issues/1388 ## Code Integrity on MacOS diff --git a/doc/api/errors.md b/doc/api/errors.md index c8d4f5260ffb65..d5f77c0df14830 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -793,6 +793,8 @@ 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. From 28f2d04ba91023d3fe684e83c5622c0465dffadc Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Mon, 11 Nov 2024 09:28:11 -0800 Subject: [PATCH 09/19] move stub calls for Linux/Darwin into js --- lib/internal/code_integrity.js | 19 ++++++++++++++++++- src/node_code_integrity.cc | 8 ++++---- src/node_code_integrity.h | 4 +++- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/lib/internal/code_integrity.js b/lib/internal/code_integrity.js index e444e04896d872..ae30a232c0c311 100644 --- a/lib/internal/code_integrity.js +++ b/lib/internal/code_integrity.js @@ -1,17 +1,27 @@ +// 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 { isWindows } = require('internal/util'); let isCodeIntegrityEnforced; let alreadyQueriedSystemCodeEnforcmentMode = false; const { isFileTrustedBySystemCodeIntegrityPolicy, - isInteractiveModeDisabled, + isInteractiveModeDisabledInternal, isSystemEnforcingCodeIntegrity, } = internalBinding('code_integrity'); function isAllowedToExecuteFile(filepath) { + // At the moment code integrity is only implemented on Windows + if (!isWindows) { + return true; + } + if (!alreadyQueriedSystemCodeEnforcmentMode) { isCodeIntegrityEnforced = isSystemEnforcingCodeIntegrity(); @@ -33,6 +43,13 @@ function isAllowedToExecuteFile(filepath) { return isFileTrustedBySystemCodeIntegrityPolicy(filepath); } +function isInteractiveModeDisabled() { + if (!isWindows) { + return false; + } + return isInteractiveModeDisabledInternal(); +} + module.exports = { isAllowedToExecuteFile, isFileTrustedBySystemCodeIntegrityPolicy, diff --git a/src/node_code_integrity.cc b/src/node_code_integrity.cc index d99361a92f5fa7..203dff5d36b822 100644 --- a/src/node_code_integrity.cc +++ b/src/node_code_integrity.cc @@ -103,7 +103,7 @@ static void IsFileTrustedBySystemCodeIntegrityPolicy( args.GetReturnValue().Set(isFileTrusted); } -static void IsInteractiveModeDisabled( +static void IsInteractiveModeDisabledInternal( const FunctionCallbackInfo& args) { CHECK_EQ(args.Length(), 0); @@ -254,8 +254,8 @@ void Initialize(Local target, SetMethod( context, target, - "isInteractiveModeDisabled", - IsInteractiveModeDisabled); + "isInteractiveModeDisabledInternal", + IsInteractiveModeDisabledInternal); SetMethod( context, @@ -266,7 +266,7 @@ void Initialize(Local target, void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(IsFileTrustedBySystemCodeIntegrityPolicy); - registry->Register(IsInteractiveModeDisabled); + registry->Register(IsInteractiveModeDisabledInternal); registry->Register(IsSystemEnforcingCodeIntegrity); } diff --git a/src/node_code_integrity.h b/src/node_code_integrity.h index d062ff6575e96a..ea3ab4cc948313 100644 --- a/src/node_code_integrity.h +++ b/src/node_code_integrity.h @@ -1,3 +1,6 @@ +// Windows API documentation for WLDP can be found at +// https://learn.microsoft.com/en-us/windows/win32/api/wldp/ + #ifndef SRC_NODE_CODE_INTEGRITY_H_ #define SRC_NODE_CODE_INTEGRITY_H_ @@ -5,7 +8,6 @@ #include -// {0xb5367df1,0xcbac,0x11cf,{0x95,0xca,0x00,0x80,0x5f,0x48,0xa1,0x92}} #define WLDP_HOST_OTHER \ {0x626cbec3, 0xe1fa, 0x4227, \ {0x98, 0x0, 0xed, 0x21, 0x2, 0x74, 0xcf, 0x7c}}; From ec6fd326103d9ded1cf39f80069f382101c59f9d Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Mon, 11 Nov 2024 09:28:44 -0800 Subject: [PATCH 10/19] fix expose internals flag location --- test/parallel/test-code-integrity.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/parallel/test-code-integrity.js b/test/parallel/test-code-integrity.js index bd3476e0bec462..aa53f3c6f72a26 100644 --- a/test/parallel/test-code-integrity.js +++ b/test/parallel/test-code-integrity.js @@ -1,7 +1,7 @@ -'use strict'; - // Flags: --expose-internals +'use strict'; + require('../common'); const assert = require('node:assert'); const { describe, it } = require('node:test'); From 253e92d35c04a62259e34f8780c1c681ff4d7a8e Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Wed, 13 Nov 2024 10:41:00 -0800 Subject: [PATCH 11/19] remove trailing whitespace --- lib/internal/code_integrity.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/code_integrity.js b/lib/internal/code_integrity.js index ae30a232c0c311..cb5cd5b69efb49 100644 --- a/lib/internal/code_integrity.js +++ b/lib/internal/code_integrity.js @@ -1,5 +1,5 @@ // Code integrity is a security feature which prevents unsigned -// code from executing. More information can be found in the docs +// code from executing. More information can be found in the docs // doc/api/code_integrity.md 'use strict'; From dd1cb3bb127ef4fd363cb6b311d92ca7e74cf2ca Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Tue, 10 Dec 2024 14:43:12 -0800 Subject: [PATCH 12/19] update documentation --- doc/api/errors.md | 2 ++ doc/api/wdac-manifest.xml | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/api/errors.md b/doc/api/errors.md index d5f77c0df14830..081cadcba4ea18 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -801,6 +801,8 @@ 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. diff --git a/doc/api/wdac-manifest.xml b/doc/api/wdac-manifest.xml index 8bab5cfb16b165..064aed76720d09 100644 --- a/doc/api/wdac-manifest.xml +++ b/doc/api/wdac-manifest.xml @@ -1,5 +1,7 @@ + - \ No newline at end of file + From c1c101f36fc91c59c3414b8d6acb7d7455e1bca6 Mon Sep 17 00:00:00 2001 From: StefanStojanovic Date: Mon, 2 Dec 2024 11:03:54 +0100 Subject: [PATCH 13/19] Fix --- lib/{internal => }/code_integrity.js | 13 ++++++++++++- lib/internal/main/eval_string.js | 2 +- lib/internal/modules/cjs/loader.js | 2 +- node.gyp | 10 ++++++++-- src/node_binding.cc | 11 +++++++++-- src/node_code_integrity.cc | 19 ------------------- src/node_code_integrity.h | 3 --- src/node_external_reference.h | 11 +++++++++-- test/parallel/test-bootstrap-modules.js | 4 ++++ test/parallel/test-code-integrity.js | 3 --- 10 files changed, 44 insertions(+), 34 deletions(-) rename lib/{internal => }/code_integrity.js (81%) diff --git a/lib/internal/code_integrity.js b/lib/code_integrity.js similarity index 81% rename from lib/internal/code_integrity.js rename to lib/code_integrity.js index cb5cd5b69efb49..d4fd6f2cc9b3c1 100644 --- a/lib/internal/code_integrity.js +++ b/lib/code_integrity.js @@ -10,11 +10,22 @@ const { isWindows } = require('internal/util'); let isCodeIntegrityEnforced; let alreadyQueriedSystemCodeEnforcmentMode = false; +// Binding stub for non-Windows platforms +let binding = { + isFileTrustedBySystemCodeIntegrityPolicy: () => true, + isInteractiveModeDisabledInternal:() => false, + isSystemEnforcingCodeIntegrity: () => false, +} +// Load the actual binding if on Windows +if (isWindows) { + binding = internalBinding('code_integrity'); +} + const { isFileTrustedBySystemCodeIntegrityPolicy, isInteractiveModeDisabledInternal, isSystemEnforcingCodeIntegrity, -} = internalBinding('code_integrity'); +} = binding; function isAllowedToExecuteFile(filepath) { // At the moment code integrity is only implemented on Windows diff --git a/lib/internal/main/eval_string.js b/lib/internal/main/eval_string.js index cd8f6ba65b8b28..be072958140189 100644 --- a/lib/internal/main/eval_string.js +++ b/lib/internal/main/eval_string.js @@ -29,7 +29,7 @@ const { }, } = require('internal/errors'); -const ci = require('internal/code_integrity'); +const ci = require('code_integrity'); if (ci.isInteractiveModeDisabled()) { throw new ERR_CODE_INTEGRITY_BLOCKED('"eval"'); } diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 6365c979388be1..9aea06f24cf600 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -217,7 +217,7 @@ const onRequire = getLazy(() => tracingChannel('module.require')); const relativeResolveCache = { __proto__: null }; -const ci = require('internal/code_integrity'); +const ci = require('code_integrity'); let requireDepth = 0; let isPreloading = false; diff --git a/node.gyp b/node.gyp index dfb01ed3d62319..93f18df2158d75 100644 --- a/node.gyp +++ b/node.gyp @@ -104,7 +104,6 @@ 'src/node_blob.cc', 'src/node_buffer.cc', 'src/node_builtins.cc', - 'src/node_code_integrity.cc', 'src/node_config.cc', 'src/node_constants.cc', 'src/node_contextify.cc', @@ -231,7 +230,6 @@ 'src/node_blob.h', 'src/node_buffer.h', 'src/node_builtins.h', - 'src/node_code_integrity.h', 'src/node_constants.h', 'src/node_context_data.h', 'src/node_contextify.h', @@ -449,6 +447,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 5a8ef378094e7e..da796f77058772 100644 --- a/src/node_binding.cc +++ b/src/node_binding.cc @@ -43,7 +43,6 @@ V(buffer) \ V(builtins) \ V(cares_wrap) \ - V(code_integrity) \ V(config) \ V(constants) \ V(contextify) \ @@ -100,13 +99,21 @@ 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) \ NODE_BUILTIN_ICU_BINDINGS(V) \ NODE_BUILTIN_PROFILER_BINDINGS(V) \ NODE_BUILTIN_DEBUG_BINDINGS(V) \ - NODE_BUILTIN_QUIC_BINDINGS(V) + NODE_BUILTIN_QUIC_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_code_integrity.cc b/src/node_code_integrity.cc index 203dff5d36b822..b87d9ca92c8d33 100644 --- a/src/node_code_integrity.cc +++ b/src/node_code_integrity.cc @@ -15,7 +15,6 @@ using v8::Value; namespace codeintegrity { -#ifdef _WIN32 static bool isWldpInitialized = false; static pfnWldpCanExecuteFile WldpCanExecuteFile; static pfnWldpGetApplicationSettingBoolean WldpGetApplicationSettingBoolean; @@ -222,24 +221,6 @@ static void IsSystemEnforcingCodeIntegrity( Boolean::New(env->isolate(), static_cast(ret))); } } -#endif // _WIN32 - -#ifndef _WIN32 -static void IsFileTrustedBySystemCodeIntegrityPolicy( - const FunctionCallbackInfo& args) { - args.GetReturnValue().Set(true); -} - -static void IsInterpretiveModeDisabled( - const FunctionCallbackInfo& args) { - args.GetReturnValue().Set(false); -} - -static void IsSystemEnforcingCodeIntegrity( - const FunctionCallbackInfo& args) { - args.GetReturnValue().Set(false); -} -#endif // ifndef _WIN32 void Initialize(Local target, Local unused, diff --git a/src/node_code_integrity.h b/src/node_code_integrity.h index ea3ab4cc948313..8c586e2e821658 100644 --- a/src/node_code_integrity.h +++ b/src/node_code_integrity.h @@ -4,8 +4,6 @@ #ifndef SRC_NODE_CODE_INTEGRITY_H_ #define SRC_NODE_CODE_INTEGRITY_H_ -#ifdef _WIN32 - #include #define WLDP_HOST_OTHER \ @@ -85,5 +83,4 @@ const UNICODE_STRING _var = \ #define E_NOTFOUND 0x80070490 #endif -#endif // _WIN32 #endif // SRC_NODE_CODE_INTEGRITY_H_ diff --git a/src/node_external_reference.h b/src/node_external_reference.h index 54abd41f1179a8..caecd197883cd9 100644 --- a/src/node_external_reference.h +++ b/src/node_external_reference.h @@ -151,7 +151,6 @@ class ExternalReferenceRegistry { V(builtins) \ V(cares_wrap) \ V(config) \ - V(code_integrity) \ V(contextify) \ V(credentials) \ V(encoding_binding) \ @@ -225,12 +224,20 @@ 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/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index d340f98b83a3d6..c6d505ec58c5d7 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -105,7 +105,11 @@ expected.beforePreExec = new Set([ 'Internal Binding wasm_web_api', 'NativeModule internal/events/abort_listener', 'NativeModule internal/modules/typescript', + 'NativeModule code_integrity', ]); +if (common.isWindows) { + 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 index aa53f3c6f72a26..ab8beddeb1b05b 100644 --- a/test/parallel/test-code-integrity.js +++ b/test/parallel/test-code-integrity.js @@ -1,8 +1,5 @@ -// Flags: --expose-internals - 'use strict'; -require('../common'); const assert = require('node:assert'); const { describe, it } = require('node:test'); const ci = require('code_integrity'); From ba5979220d9cd79ff27a4fc9b52b586959d28b64 Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Thu, 23 Jan 2025 08:56:21 -0800 Subject: [PATCH 14/19] move global variables into per_process namespace. fix formatting --- lib/code_integrity.js | 4 +-- src/node_code_integrity.cc | 45 +++++++++++++++------------- test/parallel/test-code-integrity.js | 1 + 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/lib/code_integrity.js b/lib/code_integrity.js index d4fd6f2cc9b3c1..ec14b534e4251b 100644 --- a/lib/code_integrity.js +++ b/lib/code_integrity.js @@ -13,9 +13,9 @@ let alreadyQueriedSystemCodeEnforcmentMode = false; // Binding stub for non-Windows platforms let binding = { isFileTrustedBySystemCodeIntegrityPolicy: () => true, - isInteractiveModeDisabledInternal:() => false, + isInteractiveModeDisabledInternal: () => false, isSystemEnforcingCodeIntegrity: () => false, -} +}; // Load the actual binding if on Windows if (isWindows) { binding = internalBinding('code_integrity'); diff --git a/src/node_code_integrity.cc b/src/node_code_integrity.cc index b87d9ca92c8d33..2407b5087bad59 100644 --- a/src/node_code_integrity.cc +++ b/src/node_code_integrity.cc @@ -13,19 +13,22 @@ using v8::Local; using v8::Object; using v8::Value; +namespace per_process { + bool isWldpInitialized = false; + pfnWldpCanExecuteFile WldpCanExecuteFile; + pfnWldpGetApplicationSettingBoolean WldpGetApplicationSettingBoolean; + pfnWldpQuerySecurityPolicy WldpQuerySecurityPolicy; +} + namespace codeintegrity { -static bool isWldpInitialized = false; -static pfnWldpCanExecuteFile WldpCanExecuteFile; -static pfnWldpGetApplicationSettingBoolean WldpGetApplicationSettingBoolean; -static pfnWldpQuerySecurityPolicy WldpQuerySecurityPolicy; static PCWSTR NODEJS = L"Node.js"; static PCWSTR ENFORCE_CODE_INTEGRITY_SETTING_NAME = L"EnforceCodeIntegrity"; static PCWSTR DISABLE_INTERPRETIVE_MODE_SETTING_NAME = L"DisableInteractiveMode"; void InitWldp(Environment* env) { - if (isWldpInitialized) { + if (per_process::isWldpInitialized) { return; } @@ -38,22 +41,22 @@ void InitWldp(Environment* env) { return env->ThrowError("Unable to load wldp.dll"); } - WldpCanExecuteFile = + per_process::WldpCanExecuteFile = (pfnWldpCanExecuteFile)GetProcAddress( wldp_module, "WldpCanExecuteFile"); - WldpGetApplicationSettingBoolean = + per_process::WldpGetApplicationSettingBoolean = (pfnWldpGetApplicationSettingBoolean)GetProcAddress( wldp_module, "WldpGetApplicationSettingBoolean"); - WldpQuerySecurityPolicy = + per_process::WldpQuerySecurityPolicy = (pfnWldpQuerySecurityPolicy)GetProcAddress( wldp_module, "WldpQuerySecurityPolicy"); - isWldpInitialized = true; + per_process::isWldpInitialized = true; } static void IsFileTrustedBySystemCodeIntegrityPolicy( @@ -62,7 +65,7 @@ static void IsFileTrustedBySystemCodeIntegrityPolicy( CHECK(args[0]->IsString()); Environment* env = Environment::GetCurrent(args); - if (!isWldpInitialized) { + if (!per_process::isWldpInitialized) { InitWldp(env); } @@ -86,7 +89,7 @@ static void IsFileTrustedBySystemCodeIntegrityPolicy( const GUID wldp_host_other = WLDP_HOST_OTHER; WLDP_EXECUTION_POLICY result; - HRESULT hr = WldpCanExecuteFile( + HRESULT hr = per_process::WldpCanExecuteFile( wldp_host_other, WLDP_EXECUTION_EVALUATION_OPTION_NONE, hFile, @@ -108,13 +111,13 @@ static void IsInteractiveModeDisabledInternal( Environment* env = Environment::GetCurrent(args); - if (!isWldpInitialized) { + if (!per_process::isWldpInitialized) { InitWldp(env); } - if (WldpGetApplicationSettingBoolean != nullptr) { + if (per_process::WldpGetApplicationSettingBoolean != nullptr) { BOOL ret; - HRESULT hr = WldpGetApplicationSettingBoolean( + HRESULT hr = per_process::WldpGetApplicationSettingBoolean( NODEJS, DISABLE_INTERPRETIVE_MODE_SETTING_NAME, &ret); @@ -137,7 +140,7 @@ static void IsInteractiveModeDisabledInternal( // 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 (WldpQuerySecurityPolicy != nullptr) { + 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"); @@ -145,7 +148,7 @@ static void IsInteractiveModeDisabledInternal( WLDP_SECURE_SETTING_VALUE_TYPE_BOOLEAN; ULONG valueSize = sizeof(int); int ret = 0; - HRESULT hr = WldpQuerySecurityPolicy( + HRESULT hr = per_process::WldpQuerySecurityPolicy( &providerName, &keyName, &valueName, @@ -168,13 +171,13 @@ static void IsSystemEnforcingCodeIntegrity( Environment* env = Environment::GetCurrent(args); - if (!isWldpInitialized) { + if (!per_process::isWldpInitialized) { InitWldp(env); } - if (WldpGetApplicationSettingBoolean != nullptr) { + if (per_process::WldpGetApplicationSettingBoolean != nullptr) { BOOL ret; - HRESULT hr = WldpGetApplicationSettingBoolean( + HRESULT hr = per_process::WldpGetApplicationSettingBoolean( NODEJS, ENFORCE_CODE_INTEGRITY_SETTING_NAME, &ret); @@ -197,7 +200,7 @@ static void IsSystemEnforcingCodeIntegrity( // 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 (WldpQuerySecurityPolicy != nullptr) { + 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"); @@ -205,7 +208,7 @@ static void IsSystemEnforcingCodeIntegrity( WLDP_SECURE_SETTING_VALUE_TYPE_BOOLEAN; ULONG valueSize = sizeof(int); int ret = 0; - HRESULT hr = WldpQuerySecurityPolicy( + HRESULT hr = per_process::WldpQuerySecurityPolicy( &providerName, &keyName, &valueName, diff --git a/test/parallel/test-code-integrity.js b/test/parallel/test-code-integrity.js index ab8beddeb1b05b..4e7674c7410c8e 100644 --- a/test/parallel/test-code-integrity.js +++ b/test/parallel/test-code-integrity.js @@ -1,5 +1,6 @@ 'use strict'; +require('../common'); const assert = require('node:assert'); const { describe, it } = require('node:test'); const ci = require('code_integrity'); From ac903dd5b07795c4be6281f74bb666c1da411e9f Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Thu, 6 Feb 2025 12:21:40 -0800 Subject: [PATCH 15/19] better docs, formatting --- lib/internal/code_integrity.js | 69 ++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 lib/internal/code_integrity.js diff --git a/lib/internal/code_integrity.js b/lib/internal/code_integrity.js new file mode 100644 index 00000000000000..ec14b534e4251b --- /dev/null +++ b/lib/internal/code_integrity.js @@ -0,0 +1,69 @@ +// 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 { isWindows } = require('internal/util'); + +let isCodeIntegrityEnforced; +let alreadyQueriedSystemCodeEnforcmentMode = false; + +// Binding stub for non-Windows platforms +let binding = { + isFileTrustedBySystemCodeIntegrityPolicy: () => true, + isInteractiveModeDisabledInternal: () => false, + isSystemEnforcingCodeIntegrity: () => false, +}; +// Load the actual binding if on Windows +if (isWindows) { + binding = internalBinding('code_integrity'); +} + +const { + isFileTrustedBySystemCodeIntegrityPolicy, + isInteractiveModeDisabledInternal, + isSystemEnforcingCodeIntegrity, +} = binding; + +function isAllowedToExecuteFile(filepath) { + // At the moment code integrity is only implemented on Windows + if (!isWindows) { + return true; + } + + 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); +} + +function isInteractiveModeDisabled() { + if (!isWindows) { + return false; + } + return isInteractiveModeDisabledInternal(); +} + +module.exports = { + isAllowedToExecuteFile, + isFileTrustedBySystemCodeIntegrityPolicy, + isInteractiveModeDisabled, + isSystemEnforcingCodeIntegrity, +}; From 36e7520bf64b00e829dd4a5025d069dcd0a49186 Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Thu, 6 Feb 2025 12:22:29 -0800 Subject: [PATCH 16/19] better docs, formatting --- doc/api/code_integrity.md | 30 ++++++++---- lib/code_integrity.js | 69 ---------------------------- lib/internal/main/eval_string.js | 2 +- lib/internal/modules/cjs/loader.js | 2 +- src/node_code_integrity.cc | 8 ++-- test/parallel/test-code-integrity.js | 4 +- 6 files changed, 31 insertions(+), 84 deletions(-) delete mode 100644 lib/code_integrity.js diff --git a/doc/api/code_integrity.md b/doc/api/code_integrity.md index f8a581cf508027..2425dac1fdbe2e 100644 --- a/doc/api/code_integrity.md +++ b/doc/api/code_integrity.md @@ -20,20 +20,29 @@ If you find a potential security vulnerability, please refer to our ## 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, +environment enforcing code integrity: the application developers, those administrating the system enforcing code integrity, and -the end user. +the end user. The following sections describe how each audience +can interact with code integrity enforcement. ### Windows Code Integrity and Application Developers -Application developers are responsible for generating and -distributing the signature information for their application. +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. +in robust ways to avoid unintended code execution. This includes +use of ```eval``` and loading modules outside of standard methods. -Application developers can generate a Windows catalog file to -store the hash of all files Node.js is expected to execute. +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 @@ -45,9 +54,14 @@ New-FileCatalog -Version 2 -CatalogFilePath MyApplicationCatalog.cat -Path \my\a 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 for the final version of the files that you intend to ship +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](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.security/set-authenticodesignature) commandlet. + ### Windows Code Integrity and System Administrators This section is intended for system administrators who want to enable Node.js diff --git a/lib/code_integrity.js b/lib/code_integrity.js deleted file mode 100644 index ec14b534e4251b..00000000000000 --- a/lib/code_integrity.js +++ /dev/null @@ -1,69 +0,0 @@ -// 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 { isWindows } = require('internal/util'); - -let isCodeIntegrityEnforced; -let alreadyQueriedSystemCodeEnforcmentMode = false; - -// Binding stub for non-Windows platforms -let binding = { - isFileTrustedBySystemCodeIntegrityPolicy: () => true, - isInteractiveModeDisabledInternal: () => false, - isSystemEnforcingCodeIntegrity: () => false, -}; -// Load the actual binding if on Windows -if (isWindows) { - binding = internalBinding('code_integrity'); -} - -const { - isFileTrustedBySystemCodeIntegrityPolicy, - isInteractiveModeDisabledInternal, - isSystemEnforcingCodeIntegrity, -} = binding; - -function isAllowedToExecuteFile(filepath) { - // At the moment code integrity is only implemented on Windows - if (!isWindows) { - return true; - } - - 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); -} - -function isInteractiveModeDisabled() { - if (!isWindows) { - return false; - } - return isInteractiveModeDisabledInternal(); -} - -module.exports = { - isAllowedToExecuteFile, - isFileTrustedBySystemCodeIntegrityPolicy, - isInteractiveModeDisabled, - isSystemEnforcingCodeIntegrity, -}; diff --git a/lib/internal/main/eval_string.js b/lib/internal/main/eval_string.js index be072958140189..cd8f6ba65b8b28 100644 --- a/lib/internal/main/eval_string.js +++ b/lib/internal/main/eval_string.js @@ -29,7 +29,7 @@ const { }, } = require('internal/errors'); -const ci = require('code_integrity'); +const ci = require('internal/code_integrity'); if (ci.isInteractiveModeDisabled()) { throw new ERR_CODE_INTEGRITY_BLOCKED('"eval"'); } diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 9aea06f24cf600..6365c979388be1 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -217,7 +217,7 @@ const onRequire = getLazy(() => tracingChannel('module.require')); const relativeResolveCache = { __proto__: null }; -const ci = require('code_integrity'); +const ci = require('internal/code_integrity'); let requireDepth = 0; let isPreloading = false; diff --git a/src/node_code_integrity.cc b/src/node_code_integrity.cc index 2407b5087bad59..d1215ae917be00 100644 --- a/src/node_code_integrity.cc +++ b/src/node_code_integrity.cc @@ -20,7 +20,7 @@ namespace per_process { pfnWldpQuerySecurityPolicy WldpQuerySecurityPolicy; } -namespace codeintegrity { +namespace code_integrity { static PCWSTR NODEJS = L"Node.js"; static PCWSTR ENFORCE_CODE_INTEGRITY_SETTING_NAME = L"EnforceCodeIntegrity"; @@ -254,10 +254,10 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(IsSystemEnforcingCodeIntegrity); } -} // namespace codeintegrity +} // namespace code_integrity } // namespace node NODE_BINDING_CONTEXT_AWARE_INTERNAL(code_integrity, - node::codeintegrity::Initialize) + node::code_integrity::Initialize) NODE_BINDING_EXTERNAL_REFERENCE(code_integrity, - node::codeintegrity::RegisterExternalReferences) + node::code_integrity::RegisterExternalReferences) diff --git a/test/parallel/test-code-integrity.js b/test/parallel/test-code-integrity.js index 4e7674c7410c8e..a88a6e2fe84e62 100644 --- a/test/parallel/test-code-integrity.js +++ b/test/parallel/test-code-integrity.js @@ -1,9 +1,11 @@ +// Flags: --expose-internals + 'use strict'; require('../common'); const assert = require('node:assert'); const { describe, it } = require('node:test'); -const ci = require('code_integrity'); +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', From ecb293bcc7c869564f6f5d898448dc003b0db499 Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Wed, 12 Feb 2025 11:29:27 -0800 Subject: [PATCH 17/19] move integrity check into module._load --- lib/internal/modules/cjs/loader.js | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 6365c979388be1..61ee67b1fc8336 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -1220,6 +1220,11 @@ 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:')) { + + const isAllowedToExecute = ci.isAllowedToExecuteFile(filename); + if (!isAllowedToExecute) { + throw new ERR_CODE_INTEGRITY_VIOLATION(filename); + } const result = loadBuiltinWithHooks(filename, url, format); if (result) { return result; @@ -1250,6 +1255,11 @@ Module._load = function(request, parent, isMain) { cachedModule[kModuleCircularVisited] = true; } + 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) { @@ -1881,10 +1891,6 @@ function getRequireESMError(mod, pkg, content, filename) { * @param {string} filename The file path of the module */ Module._extensions['.js'] = function(module, filename) { - const isAllowedToExecute = ci.isAllowedToExecuteFile(filename); - if (!isAllowedToExecute) { - throw new ERR_CODE_INTEGRITY_VIOLATION(filename); - } let format, pkg; if (StringPrototypeEndsWith(filename, '.cjs')) { @@ -1915,11 +1921,6 @@ Module._extensions['.js'] = function(module, filename) { */ Module._extensions['.json'] = function(module, filename) { - const isAllowedToExecute = ci.isAllowedToExecuteFile(filename); - if (!isAllowedToExecute) { - throw new ERR_CODE_INTEGRITY_VIOLATION(filename); - } - const { source: content } = loadSource(module, filename, 'json'); try { @@ -1936,11 +1937,8 @@ Module._extensions['.json'] = function(module, filename) { * @param {string} filename The file path of the module */ Module._extensions['.node'] = function(module, filename) { - const isAllowedToExecute = ci.isAllowedToExecuteFile(filename); - if (!isAllowedToExecute) { - throw new ERR_CODE_INTEGRITY_VIOLATION(filename); - } - // Be aware this doesn't use `content` + + // Be aware this doesn't use `content` return process.dlopen(module, path.toNamespacedPath(filename)); }; From 163f7d0ca6ae889cc7685af79fac739bf592ae5b Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Thu, 13 Feb 2025 11:06:59 -0800 Subject: [PATCH 18/19] fixes for failing linting --- doc/api/code_integrity.md | 6 +++--- test/parallel/test-bootstrap-modules.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/api/code_integrity.md b/doc/api/code_integrity.md index 2425dac1fdbe2e..609e5e564f1b5e 100644 --- a/doc/api/code_integrity.md +++ b/doc/api/code_integrity.md @@ -22,7 +22,7 @@ If you find a potential security vulnerability, please refer to our 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. +signing time. There are three audiences that are involved when using Node.js in an environment enforcing code integrity: the application developers, @@ -60,7 +60,7 @@ Be sure that the catalog is generated using the final version of the files that 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](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.security/set-authenticodesignature) commandlet. +This can be done with the [Set-AuthenticodeSignature commandlet](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.security/set-authenticodesignature). ### Windows Code Integrity and System Administrators @@ -121,7 +121,7 @@ On older Windows versions, use the `Settings` section of your WDAC Policy. 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: https://github.com/nodejs/security-wg/issues/1388 +can be found here: ## Code Integrity on MacOS diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index c6d505ec58c5d7..6ada1b27591ef0 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -105,7 +105,7 @@ expected.beforePreExec = new Set([ 'Internal Binding wasm_web_api', 'NativeModule internal/events/abort_listener', 'NativeModule internal/modules/typescript', - 'NativeModule code_integrity', + 'NativeModule internal/code_integrity', ]); if (common.isWindows) { expected.beforePreExec.add('Internal Binding code_integrity'); From b349c25bc96f66d05810512a649fb8031889ec80 Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Fri, 14 Feb 2025 15:23:11 -0800 Subject: [PATCH 19/19] run linter on md file --- doc/api/code_integrity.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/code_integrity.md b/doc/api/code_integrity.md index 609e5e564f1b5e..dd2e4e30d1a202 100644 --- a/doc/api/code_integrity.md +++ b/doc/api/code_integrity.md @@ -37,7 +37,7 @@ 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 -use of ```eval``` and loading modules outside of standard methods. +use of `eval` and 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