Skip to content
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

src: add WDAC integration (Windows) #54364

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
117 changes: 117 additions & 0 deletions doc/api/code_integrity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Code Integrity

rdw-msft marked this conversation as resolved.
Show resolved Hide resolved
<!-- 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

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.

### Windows Code Integrity and Application Developers

Application developers are responsible for generating and
distributing the signature information for their application.
Application developers are also expected to design their application
in robust ways to avoid unintended code execution.

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 for the final version of the files that you intend to ship
(i.e. after minifying).

### 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
14 changes: 14 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,20 @@ 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>

Copy link
Member

Choose a reason for hiding this comment

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

Same stability index applies. Stability: 1.1

### `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`

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
5 changes: 5 additions & 0 deletions doc/api/wdac-manifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
Copy link
Member

Choose a reason for hiding this comment

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

A comment in this document explaining what this is and why it is here would be helpful.

<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>
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
</AppManifest>
</AppManifest>

58 changes: 58 additions & 0 deletions lib/internal/code_integrity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// 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';
Copy link
Member

Choose a reason for hiding this comment

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

A comment here explaining a bit about what this is would be helpful. Also, if this feature is limited to windows, comments indicating so should be added as well as possible asserts to ensure it is only running on windows.

Copy link
Member

Choose a reason for hiding this comment

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

I think this file should throw if required by a non-windows environment.


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

let isCodeIntegrityEnforced;
let alreadyQueriedSystemCodeEnforcmentMode = false;

const {
isFileTrustedBySystemCodeIntegrityPolicy,
isInteractiveModeDisabledInternal,
isSystemEnforcingCodeIntegrity,
} = internalBinding('code_integrity');

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();
}

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 @@ -18,6 +18,17 @@ const { addBuiltinLibsToObject } = require('internal/modules/helpers');
const { stripTypeScriptModuleTypes } = require('internal/modules/typescript');
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"');
}

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

const {
codes: {
ERR_CODE_INTEGRITY_VIOLATION,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_MODULE_SPECIFIER,
ERR_REQUIRE_CYCLE_MODULE,
Expand Down Expand Up @@ -199,6 +200,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 @@ -1677,6 +1680,11 @@ function getRequireESMError(mod, pkg, content, filename) {
* @param {string} filename The file path of the module
*/
Module._extensions['.js'] = function(module, filename) {
const isAllowedToExecute = ci.isAllowedToExecuteFile(filename);
Copy link
Member

Choose a reason for hiding this comment

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

This might be better placed inside Module._load (search for permission.isEnabled() on this file).

Copy link
Member

Choose a reason for hiding this comment

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

We shouldn't even call this function for non-windows environments.

if (!isAllowedToExecute) {
throw new ERR_CODE_INTEGRITY_VIOLATION(filename);
}

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

};

/**
Expand All @@ -1704,6 +1713,11 @@ Module._extensions['.js'] = function(module, filename) {
* @param {string} filename The file path of the module
*/
Module._extensions['.json'] = function(module, filename) {
const isAllowedToExecute = ci.isAllowedToExecuteFile(filename);
if (!isAllowedToExecute) {
throw new ERR_CODE_INTEGRITY_VIOLATION(filename);
}

const content = fs.readFileSync(filename, 'utf8');

try {
Expand All @@ -1720,6 +1734,10 @@ Module._extensions['.json'] = function(module, filename) {
* @param {string} filename The file path of the module
*/
Module._extensions['.node'] = function(module, filename) {
const isAllowedToExecute = ci.isAllowedToExecuteFile(filename);
if (!isAllowedToExecute) {
throw new ERR_CODE_INTEGRITY_VIOLATION(filename);
}
// Be aware this doesn't use `content`
return process.dlopen(module, path.toNamespacedPath(filename));
};
Expand Down
2 changes: 2 additions & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
'src/node_blob.cc',
'src/node_buffer.cc',
'src/node_builtins.cc',
'src/node_code_integrity.cc',
'src/node_config.cc',
'src/node_constants.cc',
'src/node_contextify.cc',
Expand Down Expand Up @@ -228,6 +229,7 @@
'src/node_blob.h',
'src/node_buffer.h',
'src/node_builtins.h',
'src/node_code_integrity.h',
'src/node_constants.h',
'src/node_context_data.h',
'src/node_contextify.h',
Expand Down
1 change: 1 addition & 0 deletions src/node_binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
V(buffer) \
V(builtins) \
V(cares_wrap) \
V(code_integrity) \
V(config) \
V(constants) \
V(contextify) \
Expand Down
Loading