Skip to content

Commit f0b67f5

Browse files
committed
src: add WDAC integration (Windows)
Add calls to Windows Defender Application Control to enforce integrity of .js, .json, .node files. add typings. fix version number add integrity checks to esm loader fix esm code integrity tests only load code integrity module on Windows address typings feedback fix readability and formatting fix naming conventions fix error in merge
1 parent e38ce27 commit f0b67f5

22 files changed

+786
-3
lines changed

doc/api/code_integrity.md

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Code Integrity
2+
3+
<!--introduced_in=REPLACEME-->
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

+16
Original file line numberDiff line numberDiff line change
@@ -794,6 +794,22 @@ changes:
794794
There was an attempt to use a `MessagePort` instance in a closed
795795
state, usually after `.close()` has been called.
796796

797+
<a id="ERR_CODE_INTEGRITY_BLOCKED"></a>
798+
799+
### `ERR_CODE_INTEGRITY_BLOCKED`
800+
801+
> Stability: 1.1 - Active development
802+
803+
Feature has been disabled due to OS Code Integrity policy.
804+
805+
<a id="ERR_CODE_INTEGRITY_VIOLATION"></a>
806+
807+
### `ERR_CODE_INTEGRITY_VIOLATION`
808+
809+
> Stability: 1.1 - Active development
810+
811+
JavaScript code intended to be executed was rejected by system code integrity policy.
812+
797813
<a id="ERR_CONSOLE_WRITABLE_STREAM"></a>
798814

799815
### `ERR_CONSOLE_WRITABLE_STREAM`

doc/api/index.md

+1
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
* [Crypto](crypto.md)

doc/api/wdac-manifest.xml

+7
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="Node.js" xmlns="urn:schemas-microsoft-com:windows-defender-application-control">
5+
<SettingDefinition Name="EnforceCodeIntegrity" Type="Bool" IgnoreAuditPolicies="false"/>
6+
<SettingDefinition Name="DisableInteractiveMode" Type="Bool" IgnoreAuditPolicies="false"/>
7+
</AppManifest>

lib/internal/code_integrity.js

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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 {
9+
isFileTrustedBySystemCodeIntegrityPolicy,
10+
isInteractiveModeDisabled,
11+
isSystemEnforcingCodeIntegrity,
12+
} = internalBinding('code_integrity');
13+
14+
let isCodeIntegrityEnforced;
15+
let alreadyQueriedSystemCodeEnforcmentMode = false;
16+
17+
function isAllowedToExecuteFile(filepath) {
18+
if (!alreadyQueriedSystemCodeEnforcmentMode) {
19+
isCodeIntegrityEnforced = isSystemEnforcingCodeIntegrity();
20+
21+
if (isCodeIntegrityEnforced) {
22+
emitWarning(
23+
'Code integrity is being enforced by system policy.' +
24+
'\nCode integrity is an experimental feature.' +
25+
' See docs for more info.',
26+
'ExperimentalWarning');
27+
}
28+
29+
alreadyQueriedSystemCodeEnforcmentMode = true;
30+
}
31+
32+
if (!isCodeIntegrityEnforced) {
33+
return true;
34+
}
35+
36+
return isFileTrustedBySystemCodeIntegrityPolicy(filepath);
37+
}
38+
39+
module.exports = {
40+
isAllowedToExecuteFile,
41+
isFileTrustedBySystemCodeIntegrityPolicy,
42+
isInteractiveModeDisabled,
43+
isSystemEnforcingCodeIntegrity,
44+
};

lib/internal/errors.js

+4
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

+14
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,24 @@ 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+
2632
prepareMainThreadExecution();
2733
addBuiltinLibsToObject(globalThis, '<eval>');
2834
markBootstrapComplete();
2935

36+
const isWindows = require('internal/util');
37+
if (isWindows) {
38+
const ci = require('internal/code_integrity');
39+
if (ci.isInteractiveModeDisabled()) {
40+
throw new ERR_CODE_INTEGRITY_BLOCKED('"eval"');
41+
}
42+
}
43+
3044
const code = getOptionValue('--eval');
3145

3246
const print = getOptionValue('--print');

lib/internal/modules/cjs/loader.js

+20
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,11 @@ const onRequire = getLazy(() => tracingChannel('module.require'));
216217

217218
const relativeResolveCache = { __proto__: null };
218219

220+
let ci;
221+
if (isWindows) {
222+
ci = require('internal/code_integrity');
223+
}
224+
219225
let requireDepth = 0;
220226
let isPreloading = false;
221227
let statCache = null;
@@ -1217,6 +1223,13 @@ Module._load = function(request, parent, isMain) {
12171223
// For backwards compatibility, if the request itself starts with node:, load it before checking
12181224
// Module._cache. Otherwise, load it after the check.
12191225
if (StringPrototypeStartsWith(request, 'node:')) {
1226+
1227+
if (isWindows) {
1228+
const isAllowedToExecute = ci.isAllowedToExecuteFile(filename);
1229+
if (!isAllowedToExecute) {
1230+
throw new ERR_CODE_INTEGRITY_VIOLATION(filename);
1231+
}
1232+
}
12201233
const result = loadBuiltinWithHooks(filename, url, format);
12211234
if (result) {
12221235
return result;
@@ -1247,6 +1260,13 @@ Module._load = function(request, parent, isMain) {
12471260
cachedModule[kModuleCircularVisited] = true;
12481261
}
12491262

1263+
if (isWindows) {
1264+
const isAllowedToExecute = ci.isAllowedToExecuteFile(filename);
1265+
if (!isAllowedToExecute) {
1266+
throw new ERR_CODE_INTEGRITY_VIOLATION(filename);
1267+
}
1268+
}
1269+
12501270
if (BuiltinModule.canBeRequiredWithoutScheme(filename)) {
12511271
const result = loadBuiltinWithHooks(filename, url, format);
12521272
if (result) {

lib/internal/modules/esm/load.js

+20-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const {
44
RegExpPrototypeExec,
55
} = primordials;
66
const {
7+
isWindows,
78
kEmptyObject,
89
} = require('internal/util');
910

@@ -13,8 +14,9 @@ const { readFileSync } = require('fs');
1314

1415
const { Buffer: { from: BufferFrom } } = require('buffer');
1516

16-
const { URL } = require('internal/url');
17+
const { URL, fileURLToPath } = require('internal/url');
1718
const {
19+
ERR_CODE_INTEGRITY_VIOLATION,
1820
ERR_INVALID_URL,
1921
ERR_UNKNOWN_MODULE_FORMAT,
2022
ERR_UNSUPPORTED_ESM_URL_SCHEME,
@@ -24,6 +26,11 @@ const {
2426
dataURLProcessor,
2527
} = require('internal/data_url');
2628

29+
let ci;
30+
if (isWindows) {
31+
ci = require('internal/code_integrity');
32+
}
33+
2734
/**
2835
* @param {URL} url URL to the module
2936
* @param {ESModuleContext} context used to decorate error messages
@@ -34,6 +41,12 @@ async function getSource(url, context) {
3441
const responseURL = href;
3542
let source;
3643
if (protocol === 'file:') {
44+
if (isWindows) {
45+
const isAllowedToExecute = ci.isAllowedToExecuteFile(fileURLToPath(url));
46+
if (!isAllowedToExecute) {
47+
throw new ERR_CODE_INTEGRITY_VIOLATION(url);
48+
}
49+
}
3750
const { readFile: readFileAsync } = require('internal/fs/promises').exports;
3851
source = await readFileAsync(url);
3952
} else if (protocol === 'data:') {
@@ -59,6 +72,12 @@ function getSourceSync(url, context) {
5972
const responseURL = href;
6073
let source;
6174
if (protocol === 'file:') {
75+
if (isWindows) {
76+
const isAllowedToExecute = ci.isAllowedToExecuteFile(fileURLToPath(url));
77+
if (!isAllowedToExecute) {
78+
throw new ERR_CODE_INTEGRITY_VIOLATION(url);
79+
}
80+
}
6281
source = readFileSync(url);
6382
} else if (protocol === 'data:') {
6483
const result = dataURLProcessor(url);

node.gyp

+9
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@
230230
'src/node_blob.h',
231231
'src/node_buffer.h',
232232
'src/node_builtins.h',
233+
'src/node_code_integrity.h',
233234
'src/node_config_file.h',
234235
'src/node_constants.h',
235236
'src/node_context_data.h',
@@ -452,6 +453,14 @@
452453
}, {
453454
'use_openssl_def%': 0,
454455
}],
456+
# Only compile node_code_integrity on Windows
457+
[ 'OS=="win"', {
458+
'node_sources': [
459+
'<(node_sources)',
460+
'src/node_code_integrity.cc',
461+
'src/node_code_integrity.h',
462+
],
463+
}],
455464
],
456465
},
457466

0 commit comments

Comments
 (0)