Skip to content

Commit 6a3f55c

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 aa444b2 commit 6a3f55c

25 files changed

Lines changed: 855 additions & 6 deletions

doc/api/code_integrity.md

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

doc/api/errors.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -805,6 +805,22 @@ changes:
805805
There was an attempt to use a `MessagePort` instance in a closed
806806
state, usually after `.close()` has been called.
807807

808+
<a id="ERR_CODE_INTEGRITY_BLOCKED"></a>
809+
810+
### `ERR_CODE_INTEGRITY_BLOCKED`
811+
812+
> Stability: 1.1 - Active development
813+
814+
Feature has been disabled due to OS Code Integrity policy.
815+
816+
<a id="ERR_CODE_INTEGRITY_VIOLATION"></a>
817+
818+
### `ERR_CODE_INTEGRITY_VIOLATION`
819+
820+
> Stability: 1.1 - Active development
821+
822+
JavaScript code intended to be executed was rejected by system code integrity policy.
823+
808824
<a id="ERR_CONSOLE_WRITABLE_STREAM"></a>
809825

810826
### `ERR_CONSOLE_WRITABLE_STREAM`

doc/api/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
* [C++ embedder API](embedding.md)
1717
* [Child processes](child_process.md)
1818
* [Cluster](cluster.md)
19+
* [Code integrity](code_integrity.md)
1920
* [Command-line options](cli.md)
2021
* [Console](console.md)
2122
* [Crypto](crypto.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="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

Lines changed: 44 additions & 0 deletions
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 alreadyQueriedSystemCodeEnforcementMode = false;
16+
17+
function isAllowedToExecuteFile(filepath) {
18+
if (!alreadyQueriedSystemCodeEnforcementMode) {
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+
alreadyQueriedSystemCodeEnforcementMode = 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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,6 +1161,10 @@ E('ERR_CHILD_PROCESS_IPC_REQUIRED',
11611161
Error);
11621162
E('ERR_CHILD_PROCESS_STDIO_MAXBUFFER', '%s maxBuffer length exceeded',
11631163
RangeError);
1164+
E('ERR_CODE_INTEGRITY_BLOCKED',
1165+
'The feature "%s" is blocked by OS Code Integrity policy', Error);
1166+
E('ERR_CODE_INTEGRITY_VIOLATION',
1167+
'The file %s did not pass OS Code Integrity validation', Error);
11641168
E('ERR_CONSOLE_WRITABLE_STREAM',
11651169
'Console expects a writable stream instance for %s', TypeError);
11661170
E('ERR_CONSTRUCT_CALL_REQUIRED', 'Class constructor %s cannot be invoked without `new`', TypeError);

lib/internal/main/eval_stdin.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,26 @@ const {
1818
readStdin,
1919
} = require('internal/process/execution');
2020

21+
const {
22+
isWindows,
23+
} = require('internal/util');
24+
25+
const {
26+
codes: {
27+
ERR_CODE_INTEGRITY_BLOCKED,
28+
},
29+
} = require('internal/errors');
30+
2131
prepareMainThreadExecution();
2232
markBootstrapComplete();
2333

34+
if (isWindows) {
35+
const ci = require('internal/code_integrity');
36+
if (ci.isInteractiveModeDisabled()) {
37+
throw new ERR_CODE_INTEGRITY_BLOCKED('"stdin"');
38+
}
39+
}
40+
2441
readStdin((code) => {
2542
// This is necessary for fork() and CJS module compilation.
2643
// TODO(joyeecheung): pass this with something really internal.

lib/internal/main/eval_string.js

Lines changed: 14 additions & 0 deletions
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/main/repl.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,27 @@ const { getOptionValue } = require('internal/options');
1818

1919
const { exitCodes: { kInvalidCommandLineArgument } } = internalBinding('errors');
2020

21+
const {
22+
isWindows,
23+
} = require('internal/util');
24+
25+
const {
26+
codes: {
27+
ERR_CODE_INTEGRITY_BLOCKED,
28+
},
29+
} = require('internal/errors');
30+
2131
prepareMainThreadExecution();
2232

2333
markBootstrapComplete();
2434

35+
if (isWindows) {
36+
const ci = require('internal/code_integrity');
37+
if (ci.isInteractiveModeDisabled()) {
38+
throw new ERR_CODE_INTEGRITY_BLOCKED('"REPL"');
39+
}
40+
}
41+
2542
if (process.env.NODE_REPL_EXTERNAL_MODULE) {
2643
require('internal/modules/cjs/loader')
2744
.wrapModuleLoad(process.env.NODE_REPL_EXTERNAL_MODULE, undefined, true);

lib/internal/modules/cjs/loader.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ const {
190190

191191
const {
192192
codes: {
193+
ERR_CODE_INTEGRITY_VIOLATION,
193194
ERR_INVALID_ARG_TYPE,
194195
ERR_INVALID_ARG_VALUE,
195196
ERR_INVALID_MODULE_SPECIFIER,
@@ -225,6 +226,11 @@ const onRequire = getLazy(() => tracingChannel('module.require'));
225226

226227
const relativeResolveCache = { __proto__: null };
227228

229+
let ci;
230+
if (isWindows) {
231+
ci = require('internal/code_integrity');
232+
}
233+
228234
let requireDepth = 0;
229235
let isPreloading = false;
230236
let statCache = null;
@@ -1201,7 +1207,20 @@ function defaultLoadImpl(filename, format) {
12011207
case 'module-typescript':
12021208
case 'commonjs-typescript':
12031209
case 'typescript': {
1204-
return fs.readFileSync(filename, 'utf8');
1210+
if (isWindows) {
1211+
const fd = fs.openSync(filename);
1212+
try {
1213+
const isAllowedToExecute = ci.isAllowedToExecuteFile(fd);
1214+
if (!isAllowedToExecute) {
1215+
throw new ERR_CODE_INTEGRITY_VIOLATION(filename);
1216+
}
1217+
return fs.readFileSync(fd, 'utf8');
1218+
} finally {
1219+
fs.closeSync(fd);
1220+
}
1221+
} else {
1222+
return fs.readFileSync(filename, 'utf8');
1223+
}
12051224
}
12061225
case 'builtin':
12071226
return null;

0 commit comments

Comments
 (0)