Skip to content

Commit 9ff4838

Browse files
committed
src: add WDAC integration (Windows)
Add calls to Windows Defender Application Control to enforce integrity of .js, .json, .node files.
1 parent e79ae1b commit 9ff4838

File tree

15 files changed

+446
-0
lines changed

15 files changed

+446
-0
lines changed

doc/api/errors.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,18 @@ changes:
789789
There was an attempt to use a `MessagePort` instance in a closed
790790
state, usually after `.close()` has been called.
791791

792+
<a id="ERR_CODE_INTEGRITY_BLOCKED"></a>
793+
794+
### `ERR_CODE_INTEGRITY_BLOCKED`
795+
796+
Feature has been disabled due to OS Code Integrity policy.
797+
798+
<a id="ERR_CODE_INTEGRITY_VIOLATION"></a>
799+
800+
### `ERR_CODE_INTEGRITY_VIOLATION`
801+
802+
Javascript code intended to be executed was rejected by system code integrity policy.
803+
792804
<a id="ERR_CONSOLE_WRITABLE_STREAM"></a>
793805

794806
### `ERR_CONSOLE_WRITABLE_STREAM`

lib/code_integrity.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use strict';
2+
3+
let isCodeIntegrityEnforced;
4+
let alreadyQueriedSystemCodeEnforcmentMode = false;
5+
6+
const {
7+
isFileTrustedBySystemCodeIntegrityPolicy,
8+
isSystemEnforcingCodeIntegrity,
9+
} = internalBinding('code_integrity');
10+
11+
function isAllowedToExecuteFile(filepath) {
12+
if (!alreadyQueriedSystemCodeEnforcmentMode) {
13+
isCodeIntegrityEnforced = isSystemEnforcingCodeIntegrity();
14+
alreadyQueriedSystemCodeEnforcmentMode = true;
15+
}
16+
17+
if (!isCodeIntegrityEnforced) {
18+
return true;
19+
}
20+
21+
return isFileTrustedBySystemCodeIntegrityPolicy(filepath);
22+
}
23+
24+
module.exports = {
25+
isAllowedToExecuteFile,
26+
isFileTrustedBySystemCodeIntegrityPolicy,
27+
isSystemEnforcingCodeIntegrity,
28+
};

lib/internal/errors.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,6 +1144,10 @@ E('ERR_CHILD_PROCESS_IPC_REQUIRED',
11441144
Error);
11451145
E('ERR_CHILD_PROCESS_STDIO_MAXBUFFER', '%s maxBuffer length exceeded',
11461146
RangeError);
1147+
E('ERR_CODE_INTEGRITY_BLOCKED',
1148+
'The feature "%s" is blocked by OS Code Integrity policy', Error);
1149+
E('ERR_CODE_INTEGRITY_VIOLATION',
1150+
'The file %s did not pass OS Code Integrity validation', Error);
11471151
E('ERR_CONSOLE_WRITABLE_STREAM',
11481152
'Console expects a writable stream instance for %s', TypeError);
11491153
E('ERR_CONTEXT_NOT_INITIALIZED', 'context used is not initialized', Error);

lib/internal/main/eval_string.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,17 @@ const { addBuiltinLibsToObject, stripTypeScriptTypes } = require('internal/modul
1818

1919
const { getOptionValue } = require('internal/options');
2020

21+
const {
22+
codes: {
23+
ERR_CODE_INTEGRITY_BLOCKED,
24+
},
25+
} = require('internal/errors');
26+
27+
const ci = require('code_integrity');
28+
if (ci.isSystemEnforcingCodeIntegrity()) {
29+
throw new ERR_CODE_INTEGRITY_BLOCKED('"eval"');
30+
}
31+
2132
prepareMainThreadExecution();
2233
addBuiltinLibsToObject(globalThis, '<eval>');
2334
markBootstrapComplete();

lib/internal/modules/cjs/loader.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ const {
165165

166166
const {
167167
codes: {
168+
ERR_CODE_INTEGRITY_VIOLATION,
168169
ERR_INVALID_ARG_VALUE,
169170
ERR_INVALID_MODULE_SPECIFIER,
170171
ERR_REQUIRE_CYCLE_MODULE,
@@ -199,6 +200,8 @@ const onRequire = getLazy(() => tracingChannel('module.require'));
199200

200201
const relativeResolveCache = { __proto__: null };
201202

203+
const ci = require('code_integrity');
204+
202205
let requireDepth = 0;
203206
let isPreloading = false;
204207
let statCache = null;
@@ -1671,6 +1674,11 @@ Module._extensions['.js'] = function(module, filename) {
16711674
// If already analyzed the source, then it will be cached.
16721675
const content = getMaybeCachedSource(module, filename);
16731676

1677+
const isAllowedToExecute = ci.isAllowedToExecuteFile(filename);
1678+
if (!isAllowedToExecute) {
1679+
throw new ERR_CODE_INTEGRITY_VIOLATION(filename);
1680+
}
1681+
16741682
let format;
16751683
if (StringPrototypeEndsWith(filename, '.js')) {
16761684
const pkg = packageJsonReader.getNearestParentPackageJSON(filename);
@@ -1717,6 +1725,11 @@ Module._extensions['.js'] = function(module, filename) {
17171725
* @param {string} filename The file path of the module
17181726
*/
17191727
Module._extensions['.json'] = function(module, filename) {
1728+
const isAllowedToExecute = ci.isAllowedToExecuteFile(filename);
1729+
if (!isAllowedToExecute) {
1730+
throw new ERR_CODE_INTEGRITY_VIOLATION(filename);
1731+
}
1732+
17201733
const content = fs.readFileSync(filename, 'utf8');
17211734

17221735
try {
@@ -1733,6 +1746,10 @@ Module._extensions['.json'] = function(module, filename) {
17331746
* @param {string} filename The file path of the module
17341747
*/
17351748
Module._extensions['.node'] = function(module, filename) {
1749+
const isAllowedToExecute = ci.isAllowedToExecuteFile(filename);
1750+
if (!isAllowedToExecute) {
1751+
throw new ERR_CODE_INTEGRITY_VIOLATION(filename);
1752+
}
17361753
// Be aware this doesn't use `content`
17371754
return process.dlopen(module, path.toNamespacedPath(filename));
17381755
};

node.gyp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@
103103
'src/node_blob.cc',
104104
'src/node_buffer.cc',
105105
'src/node_builtins.cc',
106+
'src/node_code_integrity.cc',
106107
'src/node_config.cc',
107108
'src/node_constants.cc',
108109
'src/node_contextify.cc',
@@ -228,6 +229,7 @@
228229
'src/node_blob.h',
229230
'src/node_buffer.h',
230231
'src/node_builtins.h',
232+
'src/node_code_integrity.h',
231233
'src/node_constants.h',
232234
'src/node_context_data.h',
233235
'src/node_contextify.h',

src/node_binding.cc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
V(buffer) \
4343
V(builtins) \
4444
V(cares_wrap) \
45+
V(code_integrity) \
4546
V(config) \
4647
V(constants) \
4748
V(contextify) \

src/node_code_integrity.cc

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
#include "node_code_integrity.h"
2+
#include "v8.h"
3+
#include "node.h"
4+
#include "env-inl.h"
5+
#include "node_external_reference.h"
6+
7+
namespace node {
8+
9+
using v8::Boolean;
10+
using v8::Context;
11+
using v8::FunctionCallbackInfo;
12+
using v8::Local;
13+
using v8::Object;
14+
using v8::Value;
15+
16+
namespace codeintegrity {
17+
18+
#ifdef _WIN32
19+
static bool isWldpInitialized = false;
20+
static pfnWldpCanExecuteFile WldpCanExecuteFile;
21+
static pfnWldpGetApplicationSettingBoolean WldpGetApplicationSettingBoolean;
22+
static pfnWldpQuerySecurityPolicy WldpQuerySecurityPolicy;
23+
static PCWSTR NODEJS = L"Node.js";
24+
static PCWSTR ENFORCE_CODE_INTEGRITY_SETTING_NAME = L"EnforceCodeIntegrity";
25+
26+
void InitWldp(Environment* env) {
27+
28+
if (isWldpInitialized)
29+
{
30+
return;
31+
}
32+
33+
HMODULE wldp_module = LoadLibraryExA(
34+
"wldp.dll",
35+
nullptr,
36+
LOAD_LIBRARY_SEARCH_SYSTEM32);
37+
38+
if (wldp_module == nullptr) {
39+
return env->ThrowError("Unable to load wldp.dll");
40+
}
41+
42+
WldpCanExecuteFile =
43+
(pfnWldpCanExecuteFile)GetProcAddress(
44+
wldp_module,
45+
"WldpCanExecuteFile");
46+
47+
WldpGetApplicationSettingBoolean =
48+
(pfnWldpGetApplicationSettingBoolean)GetProcAddress(
49+
wldp_module,
50+
"WldpGetApplicationSettingBoolean");
51+
52+
WldpQuerySecurityPolicy =
53+
(pfnWldpQuerySecurityPolicy)GetProcAddress(
54+
wldp_module,
55+
"WldpQuerySecurityPolicy");
56+
57+
isWldpInitialized = true;
58+
}
59+
60+
static void IsFileTrustedBySystemCodeIntegrityPolicy(
61+
const FunctionCallbackInfo<Value>& args) {
62+
CHECK_EQ(args.Length(), 1);
63+
CHECK(args[0]->IsString());
64+
65+
Environment* env = Environment::GetCurrent(args);
66+
if (!isWldpInitialized) {
67+
InitWldp(env);
68+
}
69+
70+
BufferValue path(env->isolate(), args[0]);
71+
if (*path == nullptr) {
72+
return env->ThrowError("path cannot be empty");
73+
}
74+
75+
HANDLE hFile = CreateFileA(
76+
*path,
77+
GENERIC_READ,
78+
FILE_SHARE_READ,
79+
nullptr,
80+
OPEN_EXISTING,
81+
FILE_ATTRIBUTE_NORMAL,
82+
nullptr);
83+
84+
if (hFile == INVALID_HANDLE_VALUE || hFile == nullptr) {
85+
return env->ThrowError("Unable to open file");
86+
}
87+
88+
const GUID wldp_host_other = WLDP_HOST_OTHER;
89+
WLDP_EXECUTION_POLICY result;
90+
HRESULT hr = WldpCanExecuteFile(
91+
wldp_host_other,
92+
WLDP_EXECUTION_EVALUATION_OPTION_NONE,
93+
hFile,
94+
NODEJS,
95+
&result);
96+
CloseHandle(hFile);
97+
98+
if (FAILED(hr)) {
99+
return env->ThrowError("WldpCanExecuteFile failed");
100+
}
101+
102+
bool isFileTrusted = (result == WLDP_EXECUTION_POLICY_ALLOWED);
103+
args.GetReturnValue().Set(isFileTrusted);
104+
}
105+
106+
static void IsSystemEnforcingCodeIntegrity(
107+
const FunctionCallbackInfo<Value>& args) {
108+
CHECK_EQ(args.Length(), 0);
109+
110+
Environment* env = Environment::GetCurrent(args);
111+
112+
if (!isWldpInitialized) {
113+
InitWldp(env);
114+
}
115+
116+
if (WldpGetApplicationSettingBoolean != nullptr)
117+
{
118+
BOOL ret;
119+
HRESULT hr = WldpGetApplicationSettingBoolean(
120+
NODEJS,
121+
ENFORCE_CODE_INTEGRITY_SETTING_NAME,
122+
&ret);
123+
124+
if (SUCCEEDED(hr)) {
125+
args.GetReturnValue().Set(
126+
Boolean::New(env->isolate(), ret));
127+
return;
128+
} else if (hr != E_NOTFOUND) {
129+
// If the setting is not found, continue through to attempt WldpQuerySecurityPolicy,
130+
// as the setting may be defined in the old settings format
131+
args.GetReturnValue().Set(Boolean::New(env->isolate(), false));
132+
return;
133+
}
134+
}
135+
136+
// WldpGetApplicationSettingBoolean is the preferred way for applications to
137+
// query security policy values. However, this method only exists on Windows
138+
// versions going back to circa Win10 2023H2. In order to support systems
139+
// older than that (down to Win10RS2), we can use the deprecated
140+
// WldpQuerySecurityPolicy
141+
if (WldpQuerySecurityPolicy != nullptr) {
142+
DECLARE_CONST_UNICODE_STRING(providerName, L"Node.js");
143+
DECLARE_CONST_UNICODE_STRING(keyName, L"Settings");
144+
DECLARE_CONST_UNICODE_STRING(valueName, L"EnforceCodeIntegrity");
145+
WLDP_SECURE_SETTING_VALUE_TYPE valueType =
146+
WLDP_SECURE_SETTING_VALUE_TYPE_BOOLEAN;
147+
ULONG valueSize = sizeof(int);
148+
int ret = 0;
149+
HRESULT hr = WldpQuerySecurityPolicy(
150+
&providerName,
151+
&keyName,
152+
&valueName,
153+
&valueType,
154+
&ret,
155+
&valueSize);
156+
if (FAILED(hr)) {
157+
args.GetReturnValue().Set(Boolean::New(env->isolate(), false));
158+
return;
159+
}
160+
161+
args.GetReturnValue().Set(
162+
Boolean::New(env->isolate(), static_cast<bool>(ret)));
163+
}
164+
}
165+
#endif // _WIN32
166+
167+
#ifndef _WIN32
168+
static void IsFileTrustedBySystemCodeIntegrityPolicy(
169+
const FunctionCallbackInfo<Value>& args) {
170+
args.GetReturnValue().Set(true);
171+
}
172+
173+
static void IsSystemEnforcingCodeIntegrity(
174+
const FunctionCallbackInfo<Value>& args) {
175+
args.GetReturnValue().Set(false);
176+
}
177+
#endif // ifndef _WIN32
178+
179+
void Initialize(Local<Object> target,
180+
Local<Value> unused,
181+
Local<Context> context,
182+
void* priv) {
183+
SetMethod(
184+
context,
185+
target,
186+
"isFileTrustedBySystemCodeIntegrityPolicy",
187+
IsFileTrustedBySystemCodeIntegrityPolicy);
188+
189+
SetMethod(
190+
context,
191+
target,
192+
"isSystemEnforcingCodeIntegrity",
193+
IsSystemEnforcingCodeIntegrity);
194+
}
195+
196+
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
197+
//BindingData::RegisterExternalReferences(registry);
198+
199+
registry->Register(IsFileTrustedBySystemCodeIntegrityPolicy);
200+
registry->Register(IsSystemEnforcingCodeIntegrity);
201+
}
202+
203+
} // namespace codeintegrity
204+
} // namespace node
205+
NODE_BINDING_CONTEXT_AWARE_INTERNAL(code_integrity,
206+
node::codeintegrity::Initialize)
207+
NODE_BINDING_EXTERNAL_REFERENCE(code_integrity,
208+
node::codeintegrity::RegisterExternalReferences)

0 commit comments

Comments
 (0)