From 77f7f5fffa29a0699f982f4c21f1b5bd384f3b44 Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Wed, 7 Aug 2024 12:36:36 -0700 Subject: [PATCH 01/14] 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 | 19 +++ 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, 448 insertions(+) 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 fcd351b6993cb5..f9e1ee1d27faa1 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 d6b2ceb5962351..0f7d377cd97bf7 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 a558185e08ddb1..e660119751ae4b 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_VALUE, ERR_INVALID_MODULE_SPECIFIER, ERR_REQUIRE_CYCLE_MODULE, @@ -215,6 +216,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; @@ -1882,6 +1885,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'; @@ -1901,6 +1909,7 @@ Module._extensions['.js'] = function(module, filename) { throw err; } module._compile(source, filename, loadedFormat); + }; /** @@ -1909,6 +1918,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 { @@ -1925,6 +1940,10 @@ 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` return process.dlopen(module, path.toNamespacedPath(filename)); }; diff --git a/node.gyp b/node.gyp index 1633ed2d832fc5..71f8463e85e879 100644 --- a/node.gyp +++ b/node.gyp @@ -103,6 +103,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', @@ -228,6 +229,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 c2ef9b36d5b296..aff99e827f7a6e 100644 --- a/src/node_binding.cc +++ b/src/node_binding.cc @@ -42,6 +42,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 8d49a119c21832..e30e8fd1986082 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 cf4e6aed36adff428ef472f9b21379ba58f54066 Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Tue, 13 Aug 2024 17:29:46 -0700 Subject: [PATCH 02/14] 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 f9e1ee1d27faa1..62ab94350f5534 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 6879062fab4a88e3a2094a0ab3982966755e11ec Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Mon, 19 Aug 2024 10:44:16 -0700 Subject: [PATCH 03/14] 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 17f2c11281b41968d7c33c9f762a3366ed513a27 Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Tue, 20 Aug 2024 13:24:51 -0700 Subject: [PATCH 04/14] 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 2b7962a562dd491fee67284e97e562982a14d440 Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Tue, 20 Aug 2024 13:25:14 -0700 Subject: [PATCH 05/14] 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 e660119751ae4b..daaf1a1fa5c212 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -216,7 +216,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 4a2d48ff78a41087355bad10fcf892e73e19d107 Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Wed, 9 Oct 2024 10:28:22 -0700 Subject: [PATCH 06/14] 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 4f0127bcdefdeb9cc4e5a340a72b811f9f14a20a Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Wed, 9 Oct 2024 10:28:51 -0700 Subject: [PATCH 07/14] 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 fb3d7e6ce61da9f331bf32fc086b5b1f84c4d590 Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Mon, 11 Nov 2024 09:23:43 -0800 Subject: [PATCH 08/14] 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 62ab94350f5534..5e9c1989411ee3 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 356bde534614781be5b195fc235f2fa175c470a8 Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Mon, 11 Nov 2024 09:28:11 -0800 Subject: [PATCH 09/14] 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 a04378605811938e13bb7ccbdfdc1ef94c19c790 Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Mon, 11 Nov 2024 09:28:44 -0800 Subject: [PATCH 10/14] 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 de10dbee911b7759b6f42986d6ae250619449d22 Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Wed, 13 Nov 2024 10:41:00 -0800 Subject: [PATCH 11/14] 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 d16688f3434c1f2b9f3387c3e7fb8a99d46390ef Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Tue, 10 Dec 2024 14:43:12 -0800 Subject: [PATCH 12/14] 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 5e9c1989411ee3..1e8b9705692126 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 37b7adc2d1331c2987d9db756e4cee753bda69ff Mon Sep 17 00:00:00 2001 From: StefanStojanovic Date: Mon, 2 Dec 2024 11:03:54 +0100 Subject: [PATCH 13/14] 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 daaf1a1fa5c212..e660119751ae4b 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -216,7 +216,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 71f8463e85e879..b0c155f5124818 100644 --- a/node.gyp +++ b/node.gyp @@ -103,7 +103,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', @@ -229,7 +228,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', @@ -468,6 +466,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 aff99e827f7a6e..e10d5418ea285c 100644 --- a/src/node_binding.cc +++ b/src/node_binding.cc @@ -42,7 +42,6 @@ V(buffer) \ V(builtins) \ V(cares_wrap) \ - V(code_integrity) \ V(config) \ V(constants) \ V(contextify) \ @@ -98,13 +97,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 e30e8fd1986082..f994925e85b354 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) \ @@ -224,12 +223,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 c75ee390dcd195..e8cb74136edb17 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -104,7 +104,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 7560a4600170dcc30be5ec626993b55de0115678 Mon Sep 17 00:00:00 2001 From: Robert Waite Date: Thu, 23 Jan 2025 08:56:21 -0800 Subject: [PATCH 14/14] 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');