Skip to content

Commit b687ea7

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 a5b3d76 commit b687ea7

23 files changed

Lines changed: 804 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 speficic 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 polcies.
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 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

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_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/modules/cjs/loader.js

Lines changed: 33 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;
@@ -1164,7 +1170,19 @@ function defaultLoadImpl(filename, format) {
11641170
case 'module-typescript':
11651171
case 'commonjs-typescript':
11661172
case 'typescript': {
1167-
return fs.readFileSync(filename, 'utf8');
1173+
let fd;
1174+
if (isWindows) {
1175+
fd = fs.openSync(filename, 0x40000000);
1176+
const isAllowedToExecute = ci.isAllowedToExecuteFile(fd);
1177+
if (!isAllowedToExecute) {
1178+
throw new ERR_CODE_INTEGRITY_VIOLATION(filename);
1179+
}
1180+
let source = fs.readFileSync(fd, 'utf8');
1181+
//fs.closeSync(fd);
1182+
return source;
1183+
} else {
1184+
return fs.readFileSync(filename, 'utf8');
1185+
}
11681186
}
11691187
case 'builtin':
11701188
return null;
@@ -1268,6 +1286,13 @@ Module._load = function(request, parent, isMain, internalResolveOptions = kEmpty
12681286
// TODO(joyeecheung): a more sensible handling is probably, if there are hooks, always go through the hooks
12691287
// first before checking the cache. Otherwise, check the cache first, then proceed to default loading.
12701288
if (request === url && StringPrototypeStartsWith(request, 'node:')) {
1289+
if (isWindows) {
1290+
const isAllowedToExecute = ci.isAllowedToExecuteFile(filename);
1291+
if (!isAllowedToExecute) {
1292+
throw new ERR_CODE_INTEGRITY_VIOLATION(filename);
1293+
}
1294+
}
1295+
12711296
const normalized = BuiltinModule.normalizeRequirableId(request);
12721297
if (normalized) { // It's a builtin module.
12731298
const { resultFromHook, builtinExports } = loadBuiltinWithHooks(normalized, url, format);
@@ -1309,6 +1334,13 @@ Module._load = function(request, parent, isMain, internalResolveOptions = kEmpty
13091334
}
13101335

13111336
if (resultFromLoadHook === undefined && BuiltinModule.canBeRequiredWithoutScheme(filename)) {
1337+
if (isWindows) {
1338+
const isAllowedToExecute = ci.isAllowedToExecuteFile(filename);
1339+
if (!isAllowedToExecute) {
1340+
throw new ERR_CODE_INTEGRITY_VIOLATION(filename);
1341+
}
1342+
}
1343+
13121344
const { resultFromHook, builtinExports } = loadBuiltinWithHooks(filename, url, format);
13131345
if (builtinExports) {
13141346
return builtinExports;

lib/internal/modules/esm/load.js

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,20 @@ const {
44
RegExpPrototypeExec,
55
} = primordials;
66
const {
7+
isWindows,
78
kEmptyObject,
89
} = require('internal/util');
910

1011
const { defaultGetFormat } = require('internal/modules/esm/get_format');
1112
const { validateAttributes, emitImportAssertionWarning } = require('internal/modules/esm/assert');
12-
const fs = require('fs');
13+
const { closeSync, openSync, readFileSync } = require('fs');
14+
const { O_EXCL } = require('fs').constants;
1315

1416
const { Buffer: { from: BufferFrom } } = require('buffer');
1517

16-
const { URL } = require('internal/url');
18+
const { URL, fileURLToPath } = require('internal/url');
1719
const {
20+
ERR_CODE_INTEGRITY_VIOLATION,
1821
ERR_INVALID_URL,
1922
ERR_UNKNOWN_MODULE_FORMAT,
2023
ERR_UNSUPPORTED_ESM_URL_SCHEME,
@@ -24,6 +27,11 @@ const {
2427
dataURLProcessor,
2528
} = require('internal/data_url');
2629

30+
let ci;
31+
if (isWindows) {
32+
ci = require('internal/code_integrity');
33+
}
34+
2735
/**
2836
* @param {URL} url URL to the module
2937
* @param {LoadContext} context used to decorate error messages
@@ -33,12 +41,27 @@ function getSourceSync(url, context) {
3341
const { protocol, href } = url;
3442
const responseURL = href;
3543
let source;
44+
let fd;
3645
if (protocol === 'file:') {
3746
// If you are reading this code to figure out how to patch Node.js module loading
3847
// behavior - DO NOT depend on the patchability in new code: Node.js
3948
// internals may stop going through the JavaScript fs module entirely.
4049
// Prefer module.registerHooks() or other more formal fs hooks released in the future.
41-
source = fs.readFileSync(url);
50+
if (isWindows) {
51+
let filePath = fileURLToPath(url);
52+
fd = openSync(filePath);
53+
54+
const isAllowedToExecute = ci.isAllowedToExecuteFile(fd);
55+
if (!isAllowedToExecute) {
56+
throw new ERR_CODE_INTEGRITY_VIOLATION(url);
57+
}
58+
source = readFileSync(fd);
59+
closeSync(fd);
60+
}
61+
else
62+
{
63+
source = readFileSync(filePath);
64+
}
4265
} else if (protocol === 'data:') {
4366
const result = dataURLProcessor(url);
4467
if (result === 'failure') {

0 commit comments

Comments
 (0)