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

Conversation

rdw-msft
Copy link

@rdw-msft rdw-msft commented Aug 14, 2024

Add calls to Windows Defender Application Control to enforce integrity of .js, .json, .node files.

Motivation

In the past I've spoken to @RafaelGSS and the Node security work group about code integrity. We feel like it's an important defense in depth feature and the removal of the --experimental-policy feature gives us room to use OS-level code integrity features. This is a first pass at a CI implementation that cooperates with the OS.

These additions add an additional layer of defense against malicious code execution on a system that is enforcing code integrity. Code Integrity enforcement mitigates the risk of malicious code modifications after signing-time. For example, they can prevent an arbitrary file-write vulnerability from turning into an RCE. These additional checks are only made if the OS in code integrity enforcement mode and has explicitly set a configuration value in their code integrity policy to have Node enforce CI.

Windows Defender Application Control (WDAC)

Official documentation: https://learn.microsoft.com/en-us/windows/security/application-security/application-control/windows-defender-application-control/wdac-and-applocker-overview

WDAC is the Windows code integrity subsystem. It enforces code integrity using digital signatures. This is straightforward for DLLs and EXEs, since they're loaded using well known entry points in the OS. However, dynamic runtimes (interpreters like Node.js or Python) make it difficult to determine whether a file being read will be executed. In order to assist dynamic runtimes, WDAC provides an interface for runtimes to call with the code they intent to execute, and WDAC will determine if the runtime is allowed to execute the code based off signature information. WDAC (wldp.dll) exposes various methods for runtimes to ask the OS questions about the code integrity policy on the system. These changes leverage three interfaces WldpCanExecuteFile, WldpGetApplicationSettingBoolean, and WldpQuerySecurityPolicy.

WldpCanExecuteFile - given a file handle, determines if the file is allowed to be executed from WDAC policy.

WldpGetApplicationSettingBoolean - Queries WDAC policy for an application-defined setting with a boolean value. This can be thought of as a more secure setting store.

WldpQuerySecurityPolicy - WdlpGetApplicationSettingBoolean is not available on all versions of Windows, so this method provides fallback behavior to query WDAC policy settings.

High-level implementation

At startup, Node will query WDAC policy to see if it should enter integrity enforcement mode. If the policy provider Node.js has set the policy setting EnforceCodeIntegrity to TRUE, then Node will call WldpCanExecuteFile when a .js, .json, or .node file is loaded using require. If this setting is not enabled, the call to WldpCanExecuteFile will not be made and execution will continue as normal.

Additionally, if EnforceCodeIntegrity is set to TRUE, we disable features that allow arbitrary code from being executed by Node (e.g. the "-e" command line option and the REPL) when the system is enforcing code integrity.

Signing files

Signatures are stored in catalog file, .cat. This catalog file can be thought of as a collection of filenames and their associated hashes. A Node application author can generate a catalog using the Powershell command New-FileCatalog documentation.

PS> New-FileCatalog -CatalogFilePath ./MyApplicationCatalog.cat -Path ./MyApplicationRelease/

The catalog then can be signed with a certificate trusted by WDAC policy using Powershell with Set-AuthenticodeSignature documentation or signtool.exe

The signed catalog can then be installed on the system MyApplication is being deployed to.

Other Questions

What about Linux?

At the moment, there is no unified code integrity subsystem that provides similar cooperative interfaces for interpreters on Linux. There are proposals in-flight and we're tracking this work and hope to keep the implementation as similar as possible across OSs.

Notable Change

Adding OS code integrity integration to mitigate unintended code execution. This feature is not enabled by default and must be opted into (see code_integrity.md for more information). When this feature is enabled, Node.js restricts loading of JavaScript modules to only those with signatures trusted by OS code integrity policy.

@nodejs-github-bot
Copy link
Collaborator

Review requested:

  • @nodejs/gyp
  • @nodejs/loaders

@nodejs-github-bot nodejs-github-bot added lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. labels Aug 14, 2024
@RafaelGSS RafaelGSS added semver-minor PRs that contain new features and should be released in the next minor version. security-wg-agenda labels Aug 14, 2024
@anonrig
Copy link
Member

anonrig commented Aug 14, 2024

Can you run the formatter?

@rdw-msft
Copy link
Author

I also want to add - this is meant to facilitate discussion regarding the general implementation. I'm not very familiar with the various module loaders and ways which code can be read off disk and executed. I assume there are gaps in my implementation and I would appreciate any expertise in the area to close them. Thanks!

@targos
Copy link
Member

targos commented Aug 14, 2024

@nodejs/platform-windows

Copy link

codecov bot commented Aug 19, 2024

Codecov Report

Attention: Patch coverage is 86.84211% with 15 lines in your changes missing coverage. Please review.

Project coverage is 90.22%. Comparing base (9aa57bf) to head (873bebe).
Report is 83 commits behind head on main.

Files with missing lines Patch % Lines
lib/internal/code_integrity.js 86.95% 9 Missing ⚠️
lib/internal/modules/esm/load.js 76.92% 3 Missing ⚠️
lib/internal/modules/cjs/loader.js 88.23% 2 Missing ⚠️
lib/internal/main/eval_string.js 90.90% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #54364      +/-   ##
==========================================
- Coverage   90.24%   90.22%   -0.03%     
==========================================
  Files         630      631       +1     
  Lines      184990   185405     +415     
  Branches    36216    36348     +132     
==========================================
+ Hits       166948   167281     +333     
- Misses      11003    11029      +26     
- Partials     7039     7095      +56     
Files with missing lines Coverage Δ
lib/internal/errors.js 97.48% <100.00%> (+<0.01%) ⬆️
src/node_binding.cc 82.67% <ø> (ø)
src/node_external_reference.h 100.00% <ø> (ø)
lib/internal/main/eval_string.js 80.89% <90.90%> (+1.41%) ⬆️
lib/internal/modules/cjs/loader.js 98.04% <88.23%> (-0.23%) ⬇️
lib/internal/modules/esm/load.js 91.12% <76.92%> (-0.83%) ⬇️
lib/internal/code_integrity.js 86.95% <86.95%> (ø)

... and 57 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Member

@RafaelGSS RafaelGSS left a comment

Choose a reason for hiding this comment

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

We've discussed on the security-wg and the next steps are:

  • Creating documentation
  • Improving the test coverage

Then we can go the a proper code review. Meanwhile, I'm tagging @nodejs/tsc for awareness.

This is a Windows specific feature that will enabled by a node key in Windows system dictionary. So, it shouldn't be considered a semver-major or a specific Node.js API.

Currently, Windows should be the only environment that makes those checks available via API. We are still evaluating the status of OSX and Linux support for that. However, it's fair to assume this PR will only implement such guarantees for Windows users.

I recommend watching our meeting as we've discussed the usage and how this is turned-on in Windows systems (https://www.youtube.com/watch?v=w4zzH-otKNI)

@l0kod
Copy link

l0kod commented Sep 11, 2024

FYI, we are working on a similar feature for Linux. The goal is the same but the implementation and API are different. Linux already provides access control systems and this new feature (previously open+O_MAYEXEC, now execveat+AT_CHECK) is the missing piece to control script execution the same way other kind of execution can already be controlled. This article gives a good overview: https://lwn.net/Articles/982085/

I plan to send a new patch series in a few weeks. Please let me know what you think.

@l0kod
Copy link

l0kod commented Sep 11, 2024

Cc @tiran, @zooba

@rdw-msft
Copy link
Author

Hey @RafaelGSS , I've added more documentation about code integrity mode. Please let me know if it looks adequate, or if you'd like me to explain more.

I also added doc/api/wdac-manifest.xml. This is the manifest we spoke about in the last meeting I attended. It declares the WDAC Application settings that NodeJS will query for. It should be hosted in an accessible place so when system admins author their Windows Code Integrity policy they know what settings are available to them, and it also adds some type-checking for the policy writer.

I'll plan on attending the next security WG meeting to discuss in more detail if needed. Thanks

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

static void IsFileTrustedBySystemCodeIntegrityPolicy(
const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(true);
}
Copy link
Member

Choose a reason for hiding this comment

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

These could probably just be implemented in JavaScript so avoid the extraneous JS->C++ boundary call.


#include <Windows.h>

// {0xb5367df1,0xcbac,0x11cf,{0x95,0xca,0x00,0x80,0x5f,0x48,0xa1,0x92}}
Copy link
Member

Choose a reason for hiding this comment

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

It's not clear what this comment is.

#ifndef SRC_NODE_CODE_INTEGRITY_H_
#define SRC_NODE_CODE_INTEGRITY_H_

#ifdef _WIN32
Copy link
Member

Choose a reason for hiding this comment

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

Given that this is Windows only, I would generally prefer that this entire internal binding only be compiled on windows, with the non-functional stubs implemented solely in javascript on the other platforms.

@github-actions github-actions bot added the request-ci-failed An error occurred while starting CI via request-ci label, and manual interventon is needed. label Apr 7, 2025
Copy link
Contributor

github-actions bot commented Apr 7, 2025

Failed to start CI
   ⚠  Commits were pushed since the last approving review:
   ⚠  - src: add WDAC integration (Windows)
   ⚠  - add typings. fix version number
   ⚠  - add integrity checks to esm loader
   ✘  Refusing to run CI on potentially unsafe PR
https://github.com/nodejs/node/actions/runs/14309779253

@RafaelGSS RafaelGSS added request-ci Add this label to start a Jenkins CI on a PR. and removed request-ci-failed An error occurred while starting CI via request-ci label, and manual interventon is needed. labels Apr 7, 2025
@github-actions github-actions bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Apr 7, 2025
@nodejs-github-bot
Copy link
Collaborator

@rdw-msft
Copy link
Author

rdw-msft commented Apr 8, 2025

getSource and getSourceSync in https://github.com/nodejs/node/blob/main/lib/internal/modules/esm/load.js

There may be other places still.

I've added integrity checks to the ESM Loader in these places.

This feature is still in active development, and I can add integrity checks to any other paths that load and execute Javascript as they are discovered. I looked over the old code integrity feature that has since been removed (policy manifests), and it looks like it only covered these two loaders as well.

@rdw-msft
Copy link
Author

I have some basic tests for the ESM loader, but I'd like to get some opinions on if there's a better way to more thoroughly cover "import" behavior to make sure all branches that load code from a local file go through this integrity check.

At the moment, I have two tests - one that checks if a simple JS file loads without error when the file passes integrity checks, and one that checks if import throws an error if the file does not pass integrity checks. These test methods just call "import" on a .js file, and the call to the OS integrity verification function is mocked to return the expected "pass" or "not pass" result.

@RafaelGSS
Copy link
Member

cc: @nodejs/loaders

@mhdawson
Copy link
Member

have @anonrig changes been incorporated yet. Waiting on that to go through the changes.

@RafaelGSS RafaelGSS added the request-ci Add this label to start a Jenkins CI on a PR. label Apr 15, 2025
@github-actions github-actions bot added request-ci-failed An error occurred while starting CI via request-ci label, and manual interventon is needed. and removed request-ci Add this label to start a Jenkins CI on a PR. labels Apr 15, 2025
Copy link
Contributor

Failed to start CI
   ⚠  Commits were pushed since the last approving review:
   ⚠  - src: add WDAC integration (Windows)
   ⚠  - add typings. fix version number
   ⚠  - add integrity checks to esm loader
   ⚠  - fix esm code integrity tests
   ✘  Refusing to run CI on potentially unsafe PR
https://github.com/nodejs/node/actions/runs/14475195356

@RafaelGSS RafaelGSS added dont-land-on-v18.x PRs that should not land on the v18.x-staging branch and should not be released in v18.x. dont-land-on-v20.x PRs that should not land on the v20.x-staging branch and should not be released in v20.x. dont-land-on-v22.x PRs that should not land on the v22.x-staging branch and should not be released in v22.x. dont-land-on-v23.x PRs that should not land on the v23.x-staging branch and should not be released in v23.x. labels Apr 15, 2025
@RafaelGSS
Copy link
Member

As discussed in the last sec wg meeting, if we land it, let's do it on the 'CURRENT' release line first, and see the adoption and feedback before porting it to an active LTS.

@RafaelGSS RafaelGSS added request-ci Add this label to start a Jenkins CI on a PR. and removed request-ci-failed An error occurred while starting CI via request-ci label, and manual interventon is needed. labels Apr 15, 2025
@github-actions github-actions bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Apr 15, 2025
@nodejs-github-bot
Copy link
Collaborator

@rdw-msft
Copy link
Author

have @anonrig changes been incorporated yet. Waiting on that to go through the changes.

Yes. The comment recommended adding a TS typing file. I did that, but I think due to some commit squashing, I can't actually pull up their comment to resolve it anymore.

Comment on lines +57 to +61
function isInteractiveModeDisabled() {
if (!isWindows) {
return false;
}
return isInteractiveModeDisabledInternal();
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?

Comment on lines +13 to +22
// 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');
}
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.

Comment on lines +32 to +35
const ci = require('internal/code_integrity');
if (ci.isInteractiveModeDisabled()) {
throw new ERR_CODE_INTEGRITY_BLOCKED('"eval"');
}
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

Comment on lines +1224 to +1227
const isAllowedToExecute = ci.isAllowedToExecuteFile(filename);
if (!isAllowedToExecute) {
throw new ERR_CODE_INTEGRITY_VIOLATION(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 shouldn't be executed for non-windows environments.

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

@@ -0,0 +1,6 @@
export interface CodeIntegrityBinding {
isAllowedToExecuteFile(filePath: string) : boolean;
Copy link
Member

Choose a reason for hiding this comment

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

The indentation is wrong.

Suggested change
isAllowedToExecuteFile(filePath: string) : boolean;
isAllowedToExecuteFile(filePath: string) : boolean;

isFileTrustedBySystemCodeIntegrityPolicy(filePath: string) : boolean;
isInteractiveModeDisabled() : boolean;
isSystemEnforcingCodeIntegrity() : boolean;
}
Copy link
Member

Choose a reason for hiding this comment

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

In order for this to work, typings/global.d.ts file need to import this module...

per_process::isWldpInitialized = true;
}

static void IsFileTrustedBySystemCodeIntegrityPolicy(
Copy link
Member

Choose a reason for hiding this comment

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

Can you add a comment to each one of these method explaining what they do?

static PCWSTR DISABLE_INTERPRETIVE_MODE_SETTING_NAME =
L"DisableInteractiveMode";

void InitWldp(Environment* env) {
Copy link
Member

Choose a reason for hiding this comment

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

Since the method names are not easily readable, it is extremely hard to understand what this method does. Can you add a comment explaining what this method do, and how it does what it does?

LoadLibraryExA("wldp.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32);

if (wldp_module == nullptr) {
return env->ThrowError("Unable to load wldp.dll");
Copy link
Member

Choose a reason for hiding this comment

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

This error needs to contain a "code" property.

@aduh95
Copy link
Contributor

aduh95 commented Apr 15, 2025

I have some basic tests for the ESM loader, but I'd like to get some opinions on if there's a better way to more thoroughly cover "import" behavior to make sure all branches that load code from a local file go through this integrity check.

I know there have been some discussions in the past for having a centralized way for all FS interactions for internal APIs to help for such cases (/cc @arcanis)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
commit-queue-squash Add this label to instruct the Commit Queue to squash all the PR commits into the first one. dont-land-on-v18.x PRs that should not land on the v18.x-staging branch and should not be released in v18.x. dont-land-on-v20.x PRs that should not land on the v20.x-staging branch and should not be released in v20.x. dont-land-on-v22.x PRs that should not land on the v22.x-staging branch and should not be released in v22.x. dont-land-on-v23.x PRs that should not land on the v23.x-staging branch and should not be released in v23.x. lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. notable-change PRs with changes that should be highlighted in changelogs. security-wg-agenda semver-minor PRs that contain new features and should be released in the next minor version. windows Issues and PRs related to the Windows platform.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants