Skip to content

Commit 44cbb21

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 a20acd4 commit 44cbb21

19 files changed

Lines changed: 682 additions & 2 deletions

doc/api/code_integrity.md

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Code Integrity
2+
3+
<!--introduced_in=v24.0.0-->
4+
5+
<!-- type=misc -->
6+
7+
> Stability: 1.1 - Active development
8+
9+
Code integrity refers to the assurance that software code has not been
10+
altered or tampered with in any unauthorized way. It ensures that
11+
the code running on a system is exactly what was intended by the developers.
12+
13+
Code integrity in Node.js integrates with platform features for code integrity
14+
policy enforcement. See platform speficic sections below for more information.
15+
16+
The Node.js threat model considers the code that the runtime executes to be
17+
trusted. As such, this feature is an additional safety belt, not a strict
18+
security boundary.
19+
20+
If you find a potential security vulnerability, please refer to our
21+
[Security Policy][].
22+
23+
## Code Integrity on Windows
24+
25+
Code integrity is an opt-in feature that leverages Window Defender Application Control
26+
to verify the code executing conforms to system policy and has not been modified since
27+
signing time.
28+
29+
There are three audiences that are involved when using Node.js in an
30+
environment enforcing code integrity: the application developers,
31+
those administrating the system enforcing code integrity, and
32+
the end user. The following sections describe how each audience
33+
can interact with code integrity enforcement.
34+
35+
### Windows Code Integrity and Application Developers
36+
37+
Windows Defender Application Control uses digital signatures to verify
38+
a file's integrity. Application developers are responsible for generating and
39+
distributing the signature information for their Node.js application.
40+
Application developers are also expected to design their application
41+
in robust ways to avoid unintended code execution. This includes
42+
use of `eval` and loading modules outside of standard methods.
43+
44+
Signature information for files which Node.js is intended to execute
45+
can be stored in a catalog file. Application developers can generate
46+
a Windows catalog file to store the hash of all files Node.js
47+
is expected to execute.
48+
49+
A catalog can be generated using the `New-FileCatalog` Powershell
50+
cmdlet. For example
51+
52+
```powershell
53+
New-FileCatalog -Version 2 -CatalogFilePath MyApplicationCatalog.cat -Path \my\application\path\
54+
```
55+
56+
The `Path` argument should point to the root folder containing your application's code. If
57+
your application's code is fully contained in one file, `Path` can point to that single file.
58+
59+
Be sure that the catalog is generated using the final version of the files that you intend to ship
60+
(i.e. after minifying).
61+
62+
The application developer should then sign the generated catalog with their Code Signing certificate
63+
to ensure the catalog is not tampered with between distribution and execution.
64+
65+
This can be done with the [Set-AuthenticodeSignature commandlet](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.security/set-authenticodesignature).
66+
67+
### Windows Code Integrity and System Administrators
68+
69+
This section is intended for system administrators who want to enable Node.js
70+
code integrity features in their environments.
71+
72+
This section assumes familiarity with managing WDAC polcies.
73+
Official documentation for WDAC can be found [here](https://learn.microsoft.com/en-us/windows/security/application-security/application-control/windows-defender-application-control/).
74+
75+
Code integrity enforcement on Windows has two toggleable settings:
76+
`EnforceCodeIntegrity` and `DisableInteractiveMode`. These settings are configured
77+
by WDAC policy.
78+
79+
`EnforceCodeIntegrity` causes Node.js to call WldpCanExecuteFile whenever a module is loaded using `require`.
80+
WldpCanExecuteFile verifies that the file's integrity has not been tampered with from signing time.
81+
The system administrator should sign and install the application's file catalog where the application
82+
is running, per WDAC guidance.
83+
84+
`DisableInteractiveMode` prevents Node.js from being run in interactive mode, and also disables the `-e` and `--eval`
85+
command line options.
86+
87+
#### Enabling Code Integrity Enforcement
88+
89+
On newer Windows versions (22H2+), the preferred method of configuring application settings is done using
90+
`AppSettings` in your WDAC Policy.
91+
92+
```text
93+
<AppSettings>
94+
<App Manifest="wdac-manifest.xml">
95+
<Setting Name="EnforceCodeIntegrity" >
96+
<Value>True</Value>
97+
</Setting>
98+
<Setting Name="DisableInteractiveMode" >
99+
<Value>True</Value>
100+
</Setting>
101+
</App>
102+
</AppSettings>
103+
```
104+
105+
On older Windows versions, use the `Settings` section of your WDAC Policy.
106+
107+
```text
108+
<Settings>
109+
<Setting Provider="Node.js" Key="Settings" ValueName="EnforceCodeIntegrity">
110+
<Value>
111+
<Boolean>true</Boolean>
112+
</Value>
113+
</Setting>
114+
<Setting Provider="Node.js" Key="Settings" ValueName="DisableInteractiveMode">
115+
<Value>
116+
<Boolean>true</Boolean>
117+
</Value>
118+
</Setting>
119+
</Settings>
120+
```
121+
122+
## Code Integrity on Linux
123+
124+
Code integrity on Linux is not yet implemented. Plans for implementation will
125+
be made once the necessary APIs on Linux have been upstreamed. More information
126+
can be found here: <https://github.com/nodejs/security-wg/issues/1388>
127+
128+
## Code Integrity on MacOS
129+
130+
Code integrity on MacOS is not yet implemented. Currently, there is no
131+
timeline for implementation.
132+
133+
[Security Policy]: https://github.com/nodejs/node/blob/main/SECURITY.md

doc/api/errors.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,22 @@ 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+
> Stability: 1.1 - Active development
797+
798+
Feature has been disabled due to OS Code Integrity policy.
799+
800+
<a id="ERR_CODE_INTEGRITY_VIOLATION"></a>
801+
802+
### `ERR_CODE_INTEGRITY_VIOLATION`
803+
804+
> Stability: 1.1 - Active development
805+
806+
JavaScript code intended to be executed was rejected by system code integrity policy.
807+
792808
<a id="ERR_CONSOLE_WRITABLE_STREAM"></a>
793809

794810
### `ERR_CONSOLE_WRITABLE_STREAM`

doc/api/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
* [C++ embedder API](embedding.md)
2020
* [Child processes](child_process.md)
2121
* [Cluster](cluster.md)
22+
* [Code integrity](code_integrity.md)
2223
* [Command-line options](cli.md)
2324
* [Console](console.md)
2425
* [Corepack](corepack.md)

doc/api/wdac-manifest.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<!-- Manifest for WDAC integration on Windows. See docs/api/code_integrity.md for
2+
more information regarding WDAC and code integrity -->
3+
<?xml version="1.0" encoding="utf-8"?>
4+
<AppManifest Id="NodeJS" xmlns="urn:schemas-microsoft-com:windows-defender-application-control">
5+
<SettingDefinition Name="EnforceCodeIntegrity" Type="Boolean" IgnoreAuditPolicies="false"/>
6+
<SettingDefinition Name="DisableInterpretiveMode" Type="Boolean" IgnoreAuditPolicies="false"/>
7+
</AppManifest>

lib/internal/code_integrity.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Code integrity is a security feature which prevents unsigned
2+
// code from executing. More information can be found in the docs
3+
// doc/api/code_integrity.md
4+
5+
'use strict';
6+
7+
const { emitWarning } = require('internal/process/warning');
8+
const { isWindows } = require('internal/util');
9+
10+
let isCodeIntegrityEnforced;
11+
let alreadyQueriedSystemCodeEnforcmentMode = false;
12+
13+
// Binding stub for non-Windows platforms
14+
let binding = {
15+
isFileTrustedBySystemCodeIntegrityPolicy: () => true,
16+
isInteractiveModeDisabledInternal: () => false,
17+
isSystemEnforcingCodeIntegrity: () => false,
18+
};
19+
// Load the actual binding if on Windows
20+
if (isWindows) {
21+
binding = internalBinding('code_integrity');
22+
}
23+
24+
const {
25+
isFileTrustedBySystemCodeIntegrityPolicy,
26+
isInteractiveModeDisabledInternal,
27+
isSystemEnforcingCodeIntegrity,
28+
} = binding;
29+
30+
function isAllowedToExecuteFile(filepath) {
31+
// At the moment code integrity is only implemented on Windows
32+
if (!isWindows) {
33+
return true;
34+
}
35+
36+
if (!alreadyQueriedSystemCodeEnforcmentMode) {
37+
isCodeIntegrityEnforced = isSystemEnforcingCodeIntegrity();
38+
39+
if (isCodeIntegrityEnforced) {
40+
emitWarning(
41+
'Code integrity is being enforced by system policy.' +
42+
'\nCode integrity is an experimental feature.' +
43+
' See docs for more info.',
44+
'ExperimentalWarning');
45+
}
46+
47+
alreadyQueriedSystemCodeEnforcmentMode = true;
48+
}
49+
50+
if (!isCodeIntegrityEnforced) {
51+
return true;
52+
}
53+
54+
return isFileTrustedBySystemCodeIntegrityPolicy(filepath);
55+
}
56+
57+
function isInteractiveModeDisabled() {
58+
if (!isWindows) {
59+
return false;
60+
}
61+
return isInteractiveModeDisabledInternal();
62+
}
63+
64+
module.exports = {
65+
isAllowedToExecuteFile,
66+
isFileTrustedBySystemCodeIntegrityPolicy,
67+
isInteractiveModeDisabled,
68+
isSystemEnforcingCodeIntegrity,
69+
};

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
@@ -23,6 +23,17 @@ const {
2323
const { addBuiltinLibsToObject } = require('internal/modules/helpers');
2424
const { getOptionValue } = require('internal/options');
2525

26+
const {
27+
codes: {
28+
ERR_CODE_INTEGRITY_BLOCKED,
29+
},
30+
} = require('internal/errors');
31+
32+
const ci = require('internal/code_integrity');
33+
if (ci.isInteractiveModeDisabled()) {
34+
throw new ERR_CODE_INTEGRITY_BLOCKED('"eval"');
35+
}
36+
2637
prepareMainThreadExecution();
2738
addBuiltinLibsToObject(globalThis, '<eval>');
2839
markBootstrapComplete();

lib/internal/modules/cjs/loader.js

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

182182
const {
183183
codes: {
184+
ERR_CODE_INTEGRITY_VIOLATION,
184185
ERR_INVALID_ARG_TYPE,
185186
ERR_INVALID_ARG_VALUE,
186187
ERR_INVALID_MODULE_SPECIFIER,
@@ -216,6 +217,8 @@ const onRequire = getLazy(() => tracingChannel('module.require'));
216217

217218
const relativeResolveCache = { __proto__: null };
218219

220+
const ci = require('internal/code_integrity');
221+
219222
let requireDepth = 0;
220223
let isPreloading = false;
221224
let statCache = null;
@@ -1217,6 +1220,11 @@ Module._load = function(request, parent, isMain) {
12171220
// For backwards compatibility, if the request itself starts with node:, load it before checking
12181221
// Module._cache. Otherwise, load it after the check.
12191222
if (StringPrototypeStartsWith(request, 'node:')) {
1223+
1224+
const isAllowedToExecute = ci.isAllowedToExecuteFile(filename);
1225+
if (!isAllowedToExecute) {
1226+
throw new ERR_CODE_INTEGRITY_VIOLATION(filename);
1227+
}
12201228
const result = loadBuiltinWithHooks(filename, url, format);
12211229
if (result) {
12221230
return result;
@@ -1247,6 +1255,11 @@ Module._load = function(request, parent, isMain) {
12471255
cachedModule[kModuleCircularVisited] = true;
12481256
}
12491257

1258+
const isAllowedToExecute = ci.isAllowedToExecuteFile(filename);
1259+
if (!isAllowedToExecute) {
1260+
throw new ERR_CODE_INTEGRITY_VIOLATION(filename);
1261+
}
1262+
12501263
if (BuiltinModule.canBeRequiredWithoutScheme(filename)) {
12511264
const result = loadBuiltinWithHooks(filename, url, format);
12521265
if (result) {
@@ -1878,6 +1891,7 @@ function getRequireESMError(mod, pkg, content, filename) {
18781891
* @param {string} filename The file path of the module
18791892
*/
18801893
Module._extensions['.js'] = function(module, filename) {
1894+
18811895
let format, pkg;
18821896
if (StringPrototypeEndsWith(filename, '.cjs')) {
18831897
format = 'commonjs';
@@ -1897,6 +1911,7 @@ Module._extensions['.js'] = function(module, filename) {
18971911
throw err;
18981912
}
18991913
module._compile(source, filename, loadedFormat);
1914+
19001915
};
19011916

19021917
/**
@@ -1905,6 +1920,7 @@ Module._extensions['.js'] = function(module, filename) {
19051920
* @param {string} filename The file path of the module
19061921
*/
19071922
Module._extensions['.json'] = function(module, filename) {
1923+
19081924
const { source: content } = loadSource(module, filename, 'json');
19091925

19101926
try {
@@ -1921,6 +1937,7 @@ Module._extensions['.json'] = function(module, filename) {
19211937
* @param {string} filename The file path of the module
19221938
*/
19231939
Module._extensions['.node'] = function(module, filename) {
1940+
19241941
// Be aware this doesn't use `content`
19251942
return process.dlopen(module, path.toNamespacedPath(filename));
19261943
};

node.gyp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@
231231
'src/node_blob.h',
232232
'src/node_buffer.h',
233233
'src/node_builtins.h',
234+
'src/node_code_integrity.h',
234235
'src/node_config_file.h',
235236
'src/node_constants.h',
236237
'src/node_context_data.h',
@@ -449,6 +450,14 @@
449450
}, {
450451
'use_openssl_def%': 0,
451452
}],
453+
# Only compile node_code_integrity on Windows
454+
[ 'OS=="win"', {
455+
'node_sources': [
456+
'<(node_sources)',
457+
'src/node_code_integrity.cc',
458+
'src/node_code_integrity.h',
459+
],
460+
}],
452461
],
453462
},
454463

src/node_binding.cc

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,20 @@
9999
V(worker) \
100100
V(zlib)
101101

102+
#define NODE_BUILTIN_OS_SPECIFIC_BINDINGS(V)
103+
104+
#ifdef _WIN32
105+
#define NODE_BUILTIN_OS_SPECIFIC_BINDINGS(V) V(code_integrity)
106+
#endif
107+
102108
#define NODE_BUILTIN_BINDINGS(V) \
103109
NODE_BUILTIN_STANDARD_BINDINGS(V) \
104110
NODE_BUILTIN_OPENSSL_BINDINGS(V) \
105111
NODE_BUILTIN_ICU_BINDINGS(V) \
106112
NODE_BUILTIN_PROFILER_BINDINGS(V) \
107113
NODE_BUILTIN_DEBUG_BINDINGS(V) \
108-
NODE_BUILTIN_QUIC_BINDINGS(V)
114+
NODE_BUILTIN_QUIC_BINDINGS(V) \
115+
NODE_BUILTIN_OS_SPECIFIC_BINDINGS(V)
109116

110117
// This is used to load built-in bindings. Instead of using
111118
// __attribute__((constructor)), we call the _register_<modname>

0 commit comments

Comments
 (0)