Skip to content

src: add WDAC integration (Windows) #54364

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions doc/api/code_integrity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Code Integrity

<!--introduced_in=REPLACEME-->

<!-- type=misc -->

> Stability: 1.1 - Active development

Code integrity refers to the assurance that software code has not been
altered or tampered with in any unauthorized way. It ensures that
the code running on a system is exactly what was intended by the developers.

Code integrity in Node.js integrates with platform features for code integrity
policy enforcement. See platform speficic sections below for more information.

The Node.js threat model considers the code that the runtime executes to be
trusted. As such, this feature is an additional safety belt, not a strict
security boundary.

If you find a potential security vulnerability, please refer to our
[Security Policy][].

## Code Integrity on Windows

Code integrity is an opt-in feature that leverages Window Defender Application Control
to verify the code executing conforms to system policy and has not been modified since
signing time.

There are three audiences that are involved when using Node.js in an
environment enforcing code integrity: the application developers,
those administrating the system enforcing code integrity, and
the end user. The following sections describe how each audience
can interact with code integrity enforcement.

### Windows Code Integrity and Application Developers

Windows Defender Application Control uses digital signatures to verify
a file's integrity. Application developers are responsible for generating and
distributing the signature information for their Node.js application.
Application developers are also expected to design their application
in robust ways to avoid unintended code execution. This includes
use of `eval` and loading modules outside of standard methods.

Signature information for files which Node.js is intended to execute
can be stored in a catalog file. Application developers can generate
a Windows catalog file to store the hash of all files Node.js
is expected to execute.

A catalog can be generated using the `New-FileCatalog` Powershell
cmdlet. For example

```powershell
New-FileCatalog -Version 2 -CatalogFilePath MyApplicationCatalog.cat -Path \my\application\path\
```

The `Path` argument should point to the root folder containing your application's code. If
your application's code is fully contained in one file, `Path` can point to that single file.

Be sure that the catalog is generated using the final version of the files that you intend to ship
(i.e. after minifying).

The application developer should then sign the generated catalog with their Code Signing certificate
to ensure the catalog is not tampered with between distribution and execution.

This can be done with the [Set-AuthenticodeSignature commandlet](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.security/set-authenticodesignature).

### Windows Code Integrity and System Administrators

This section is intended for system administrators who want to enable Node.js
code integrity features in their environments.

This section assumes familiarity with managing WDAC polcies.
Official documentation for WDAC can be found [here](https://learn.microsoft.com/en-us/windows/security/application-security/application-control/windows-defender-application-control/).

Code integrity enforcement on Windows has two toggleable settings:
`EnforceCodeIntegrity` and `DisableInteractiveMode`. These settings are configured
by WDAC policy.

`EnforceCodeIntegrity` causes Node.js to call WldpCanExecuteFile whenever a module is loaded using `require`.
WldpCanExecuteFile verifies that the file's integrity has not been tampered with from signing time.
The system administrator should sign and install the application's file catalog where the application
is running, per WDAC guidance.

`DisableInteractiveMode` prevents Node.js from being run in interactive mode, and also disables the `-e` and `--eval`
command line options.

#### Enabling Code Integrity Enforcement

On newer Windows versions (22H2+), the preferred method of configuring application settings is done using
`AppSettings` in your WDAC Policy.

```text
<AppSettings>
<App Manifest="wdac-manifest.xml">
<Setting Name="EnforceCodeIntegrity" >
<Value>True</Value>
</Setting>
<Setting Name="DisableInteractiveMode" >
<Value>True</Value>
</Setting>
</App>
</AppSettings>
```

On older Windows versions, use the `Settings` section of your WDAC Policy.

```text
<Settings>
<Setting Provider="Node.js" Key="Settings" ValueName="EnforceCodeIntegrity">
<Value>
<Boolean>true</Boolean>
</Value>
</Setting>
<Setting Provider="Node.js" Key="Settings" ValueName="DisableInteractiveMode">
<Value>
<Boolean>true</Boolean>
</Value>
</Setting>
</Settings>
```

## Code Integrity on Linux

Code integrity on Linux is not yet implemented. Plans for implementation will
be made once the necessary APIs on Linux have been upstreamed. More information
can be found here: <https://github.com/nodejs/security-wg/issues/1388>

## Code Integrity on MacOS

Code integrity on MacOS is not yet implemented. Currently, there is no
timeline for implementation.

[Security Policy]: https://github.com/nodejs/node/blob/main/SECURITY.md
16 changes: 16 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,22 @@ changes:
There was an attempt to use a `MessagePort` instance in a closed
state, usually after `.close()` has been called.

<a id="ERR_CODE_INTEGRITY_BLOCKED"></a>

### `ERR_CODE_INTEGRITY_BLOCKED`

> Stability: 1.1 - Active development

Feature has been disabled due to OS Code Integrity policy.

<a id="ERR_CODE_INTEGRITY_VIOLATION"></a>

### `ERR_CODE_INTEGRITY_VIOLATION`

> Stability: 1.1 - Active development

JavaScript code intended to be executed was rejected by system code integrity policy.

<a id="ERR_CONSOLE_WRITABLE_STREAM"></a>

### `ERR_CONSOLE_WRITABLE_STREAM`
Expand Down
1 change: 1 addition & 0 deletions doc/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* [C++ embedder API](embedding.md)
* [Child processes](child_process.md)
* [Cluster](cluster.md)
* [Code integrity](code_integrity.md)
* [Command-line options](cli.md)
* [Console](console.md)
* [Crypto](crypto.md)
Expand Down
7 changes: 7 additions & 0 deletions doc/api/wdac-manifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<!-- Manifest for WDAC integration on Windows. See docs/api/code_integrity.md for
more information regarding WDAC and code integrity -->
<?xml version="1.0" encoding="utf-8"?>
<AppManifest Id="NodeJS" xmlns="urn:schemas-microsoft-com:windows-defender-application-control">
<SettingDefinition Name="EnforceCodeIntegrity" Type="Boolean" IgnoreAuditPolicies="false"/>
<SettingDefinition Name="DisableInterpretiveMode" Type="Boolean" IgnoreAuditPolicies="false"/>
</AppManifest>
69 changes: 69 additions & 0 deletions lib/internal/code_integrity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Code integrity is a security feature which prevents unsigned
// code from executing. More information can be found in the docs
// doc/api/code_integrity.md

'use strict';

const { emitWarning } = require('internal/process/warning');
const { isWindows } = require('internal/util');

let isCodeIntegrityEnforced;
let alreadyQueriedSystemCodeEnforcmentMode = false;

// Binding stub for non-Windows platforms
let binding = {
isFileTrustedBySystemCodeIntegrityPolicy: () => true,
isInteractiveModeDisabledInternal: () => false,
isSystemEnforcingCodeIntegrity: () => false,
};
// Load the actual binding if on Windows
if (isWindows) {
binding = internalBinding('code_integrity');
}
Comment on lines +13 to +22
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO: We shouldn't even call this file or any of the methods for non-windows environments.


const {
isFileTrustedBySystemCodeIntegrityPolicy,
isInteractiveModeDisabledInternal,
isSystemEnforcingCodeIntegrity,
} = binding;

function isAllowedToExecuteFile(filepath) {
// At the moment code integrity is only implemented on Windows
if (!isWindows) {
return true;
}

if (!alreadyQueriedSystemCodeEnforcmentMode) {
isCodeIntegrityEnforced = isSystemEnforcingCodeIntegrity();

if (isCodeIntegrityEnforced) {
emitWarning(
'Code integrity is being enforced by system policy.' +
'\nCode integrity is an experimental feature.' +
' See docs for more info.',
'ExperimentalWarning');
}

alreadyQueriedSystemCodeEnforcmentMode = true;
}

if (!isCodeIntegrityEnforced) {
return true;
}

return isFileTrustedBySystemCodeIntegrityPolicy(filepath);
}

function isInteractiveModeDisabled() {
if (!isWindows) {
return false;
}
return isInteractiveModeDisabledInternal();
Comment on lines +57 to +61
Copy link
Member

@anonrig anonrig Apr 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this adds an additional item to the stack trace whenever this function is called. we should not call this method on non-windows to avoid polluting the stack trace.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, can't this method be cached?

}

module.exports = {
isAllowedToExecuteFile,
isFileTrustedBySystemCodeIntegrityPolicy,
isInteractiveModeDisabled,
isSystemEnforcingCodeIntegrity,
};
4 changes: 4 additions & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1144,6 +1144,10 @@ E('ERR_CHILD_PROCESS_IPC_REQUIRED',
Error);
E('ERR_CHILD_PROCESS_STDIO_MAXBUFFER', '%s maxBuffer length exceeded',
RangeError);
E('ERR_CODE_INTEGRITY_BLOCKED',
'The feature "%s" is blocked by OS Code Integrity policy', Error);
E('ERR_CODE_INTEGRITY_VIOLATION',
'The file %s did not pass OS Code Integrity validation', Error);
E('ERR_CONSOLE_WRITABLE_STREAM',
'Console expects a writable stream instance for %s', TypeError);
E('ERR_CONTEXT_NOT_INITIALIZED', 'context used is not initialized', Error);
Expand Down
11 changes: 11 additions & 0 deletions lib/internal/main/eval_string.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ const {
const { addBuiltinLibsToObject } = require('internal/modules/helpers');
const { getOptionValue } = require('internal/options');

const {
codes: {
ERR_CODE_INTEGRITY_BLOCKED,
},
} = require('internal/errors');

const ci = require('internal/code_integrity');
if (ci.isInteractiveModeDisabled()) {
throw new ERR_CODE_INTEGRITY_BLOCKED('"eval"');
}
Comment on lines +32 to +35
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be surrounded by isWindows check and we shouldn't even require this file for non-windows environments


prepareMainThreadExecution();
addBuiltinLibsToObject(globalThis, '<eval>');
markBootstrapComplete();
Expand Down
17 changes: 17 additions & 0 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ const {

const {
codes: {
ERR_CODE_INTEGRITY_VIOLATION,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_MODULE_SPECIFIER,
Expand Down Expand Up @@ -216,6 +217,8 @@ const onRequire = getLazy(() => tracingChannel('module.require'));

const relativeResolveCache = { __proto__: null };

const ci = require('internal/code_integrity');

let requireDepth = 0;
let isPreloading = false;
let statCache = null;
Expand Down Expand Up @@ -1217,6 +1220,11 @@ Module._load = function(request, parent, isMain) {
// For backwards compatibility, if the request itself starts with node:, load it before checking
// Module._cache. Otherwise, load it after the check.
if (StringPrototypeStartsWith(request, 'node:')) {

const isAllowedToExecute = ci.isAllowedToExecuteFile(filename);
if (!isAllowedToExecute) {
throw new ERR_CODE_INTEGRITY_VIOLATION(filename);
}
Comment on lines +1224 to +1227
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be executed for non-windows environments.

const result = loadBuiltinWithHooks(filename, url, format);
if (result) {
return result;
Expand Down Expand Up @@ -1247,6 +1255,11 @@ Module._load = function(request, parent, isMain) {
cachedModule[kModuleCircularVisited] = true;
}

const isAllowedToExecute = ci.isAllowedToExecuteFile(filename);
if (!isAllowedToExecute) {
throw new ERR_CODE_INTEGRITY_VIOLATION(filename);
}

if (BuiltinModule.canBeRequiredWithoutScheme(filename)) {
const result = loadBuiltinWithHooks(filename, url, format);
if (result) {
Expand Down Expand Up @@ -1878,6 +1891,7 @@ function getRequireESMError(mod, pkg, content, filename) {
* @param {string} filename The file path of the module
*/
Module._extensions['.js'] = function(module, filename) {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These unnecessary changes needs to be removed

let format, pkg;
if (StringPrototypeEndsWith(filename, '.cjs')) {
format = 'commonjs';
Expand All @@ -1897,6 +1911,7 @@ Module._extensions['.js'] = function(module, filename) {
throw err;
}
module._compile(source, filename, loadedFormat);

};

/**
Expand All @@ -1905,6 +1920,7 @@ Module._extensions['.js'] = function(module, filename) {
* @param {string} filename The file path of the module
*/
Module._extensions['.json'] = function(module, filename) {

const { source: content } = loadSource(module, filename, 'json');

try {
Expand All @@ -1921,6 +1937,7 @@ Module._extensions['.json'] = function(module, filename) {
* @param {string} filename The file path of the module
*/
Module._extensions['.node'] = function(module, filename) {

// Be aware this doesn't use `content`
return process.dlopen(module, path.toNamespacedPath(filename));
};
Expand Down
14 changes: 13 additions & 1 deletion lib/internal/modules/esm/load.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ const { readFileSync } = require('fs');

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

const { URL } = require('internal/url');
const { URL, fileURLToPath } = require('internal/url');
const {
ERR_CODE_INTEGRITY_VIOLATION,
ERR_INVALID_URL,
ERR_UNKNOWN_MODULE_FORMAT,
ERR_UNSUPPORTED_ESM_URL_SCHEME,
Expand All @@ -24,6 +25,8 @@ const {
dataURLProcessor,
} = require('internal/data_url');

const ci = require('internal/code_integrity');

/**
* @param {URL} url URL to the module
* @param {ESModuleContext} context used to decorate error messages
Expand All @@ -34,6 +37,11 @@ async function getSource(url, context) {
const responseURL = href;
let source;
if (protocol === 'file:') {
const isAllowedToExecute = ci.isAllowedToExecuteFile(fileURLToPath(url));
if (!isAllowedToExecute) {
throw new ERR_CODE_INTEGRITY_VIOLATION(url);
}
Comment on lines +40 to +43
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change shouldn't be executed for non-windows environments. For example, for non-windows environments, this change introduces an unnecessary fileURLToPath calls, which impacts performance for all environments.


const { readFile: readFileAsync } = require('internal/fs/promises').exports;
source = await readFileAsync(url);
} else if (protocol === 'data:') {
Expand All @@ -59,6 +67,10 @@ function getSourceSync(url, context) {
const responseURL = href;
let source;
if (protocol === 'file:') {
const isAllowedToExecute = ci.isAllowedToExecuteFile(fileURLToPath(url));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

if (!isAllowedToExecute) {
throw new ERR_CODE_INTEGRITY_VIOLATION(url);
}
source = readFileSync(url);
} else if (protocol === 'data:') {
const result = dataURLProcessor(url);
Expand Down
Loading
Loading