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 14 commits into
base: main
Choose a base branch
from
Open
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
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 it should be opted-in for - behind a flag - to users. This documentation should first describe what they should do in the terminal such as starting nodejs with --code-integrity then describing nuances of the code integrity.

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
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
## Code Integrity on Windows
## Code Integrity on Windows
<!-- YAML
added: REPLACEME
-->


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

RafaelGSS marked this conversation as resolved.
Show resolved Hide resolved
### `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
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"?>
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>
69 changes: 69 additions & 0 deletions lib/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) {
Copy link
Member

Choose a reason for hiding this comment

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

Should we also check the Windows version or do all supported Windows platforms have it?

binding = internalBinding('code_integrity');
}

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

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('code_integrity');
if (ci.isInteractiveModeDisabled()) {
throw new ERR_CODE_INTEGRITY_BLOCKED('"eval"');
}

prepareMainThreadExecution();
addBuiltinLibsToObject(globalThis, '<eval>');
markBootstrapComplete();
Expand Down
19 changes: 19 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_VALUE,
ERR_INVALID_MODULE_SPECIFIER,
ERR_REQUIRE_CYCLE_MODULE,
Expand Down Expand Up @@ -215,6 +216,8 @@ const onRequire = getLazy(() => tracingChannel('module.require'));

const relativeResolveCache = { __proto__: null };

const ci = require('code_integrity');
Copy link
Member

Choose a reason for hiding this comment

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

I would put this into an internal module right now.

Copy link
Author

Choose a reason for hiding this comment

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

Just to make sure - this just involves moving it into the lib/internal/ folder? Let me know if I need to do anything else!

Copy link
Member

Choose a reason for hiding this comment

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

Yes @rdw-msft. So it won't be exposed as a module to users (unless they pass --expose-internals)


let requireDepth = 0;
let isPreloading = false;
let statCache = null;
Expand Down Expand Up @@ -1882,6 +1885,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 @@ -1901,6 +1909,7 @@ Module._extensions['.js'] = function(module, filename) {
throw err;
}
module._compile(source, filename, loadedFormat);

};

/**
Expand All @@ -1909,6 +1918,12 @@ 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);
Copy link
Member

Choose a reason for hiding this comment

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

As I said earlier. It would be better placed inside Module._load so you don't need to replicate it to every extension.

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

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

try {
Expand All @@ -1925,6 +1940,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
8 changes: 8 additions & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,14 @@
}, {
'use_openssl_def%': 0,
}],
# Only compile node_code_integrity on Windows
[ 'OS=="win"', {
'node_sources': [
'<(node_sources)',
'src/node_code_integrity.cc',
'src/node_code_integrity.h',
],
}],
],
},

Expand Down
10 changes: 9 additions & 1 deletion src/node_binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,21 @@
V(worker) \
V(zlib)

#define NODE_BUILTIN_OS_SPECIFIC_BINDINGS(V)

#ifdef _WIN32
#define NODE_BUILTIN_OS_SPECIFIC_BINDINGS(V) \
V(code_integrity)
#endif

#define NODE_BUILTIN_BINDINGS(V) \
NODE_BUILTIN_STANDARD_BINDINGS(V) \
NODE_BUILTIN_OPENSSL_BINDINGS(V) \
NODE_BUILTIN_ICU_BINDINGS(V) \
NODE_BUILTIN_PROFILER_BINDINGS(V) \
NODE_BUILTIN_DEBUG_BINDINGS(V) \
NODE_BUILTIN_QUIC_BINDINGS(V)
NODE_BUILTIN_QUIC_BINDINGS(V) \
NODE_BUILTIN_OS_SPECIFIC_BINDINGS(V)

// This is used to load built-in bindings. Instead of using
// __attribute__((constructor)), we call the _register_<modname>
Expand Down
Loading
Loading