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 1 commit into
base: main
Choose a base branch
from

Conversation

rdw-msft
Copy link

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.

@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

@@ -198,6 +199,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)

Copy link

codecov bot commented Aug 19, 2024

Codecov Report

Attention: Patch coverage is 89.10891% with 11 lines in your changes missing coverage. Please review.

Project coverage is 90.21%. Comparing base (fd45383) to head (56aad2c).
Report is 51 commits behind head on main.

Files with missing lines Patch % Lines
lib/internal/code_integrity.js 86.95% 9 Missing ⚠️
lib/internal/main/eval_string.js 90.90% 1 Missing ⚠️
lib/internal/modules/cjs/loader.js 94.11% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #54364      +/-   ##
==========================================
- Coverage   90.26%   90.21%   -0.05%     
==========================================
  Files         630      631       +1     
  Lines      184634   185267     +633     
  Branches    36137    36234      +97     
==========================================
+ Hits       166654   167138     +484     
- Misses      11022    11129     +107     
- Partials     6958     7000      +42     
Files with missing lines Coverage Δ
lib/internal/errors.js 97.48% <100.00%> (+<0.01%) ⬆️
src/node_binding.cc 82.67% <ø> (-0.99%) ⬇️
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.08% <94.11%> (-0.18%) ⬇️
lib/internal/code_integrity.js 86.95% <86.95%> (ø)

... and 69 files with indirect coverage changes

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.

'use strict';

// Flags: --expose-internals
Copy link
Member

Choose a reason for hiding this comment

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

Nit:

Suggested change
'use strict';
// Flags: --expose-internals
// Flags: --expose-internals
'use strict';

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.

I’m concerned about synchronous calls blocking the event loop and ensuring compatibility with the Windows API across all Node.js versions supported on Windows

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

Choose a reason for hiding this comment

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

We might want to link to the original issue


namespace codeintegrity {

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

Does it work on all supported Windows environments?

Copy link
Author

Choose a reason for hiding this comment

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

WldpCanExecuteFile, WldpQueryApplicationSettingBoolean methods are implemented on Windows Server and Windows Client.

Is there a list of NodeJS supported environments I can check against?

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Author

Choose a reason for hiding this comment

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

What is the minimum version of Windows supported by Node?

Wldp.dll was added in Windows 8, however the APIs we're using to enforce file signatures were added in Windows 11 Build 22621

The APIs we're using:
WldpCanExecuteFile, checks if the file meets code integrity policy (e.g. hash is unchanged), is available in Windows 11 Build 22621+
WldpGetApplicationSettingBoolean, queries the security policy to see if a configuration setting is set, this is the flag we use to determine if we should be enforcing code integrity with calls to WldpCanExecuteFile, is also in Win11.
WldpQuerySecurityPolicy, is the fallback method we use if WldpGetApplicationSettingBoolean is not available. This allows backward compatibility to early Windows 10 builds.

We can assume that if we can't get a handle on Wldp.dll, the system is too old to support code integrity, so execution can just continue.

If we're on a system with WldpQuerySecurityPolicy, and the Node.js EnforceCodeIntegrity setting is enabled, but we can't load WldpCanExecuteFile, we should probably halt execution. I can add this behavior in the next iteration if it seems reasonable.

Copy link
Member

@RafaelGSS RafaelGSS Nov 11, 2024

Choose a reason for hiding this comment

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

cc: @nodejs/build @nodejs/platform-windows


const GUID wldp_host_other = WLDP_HOST_OTHER;
WLDP_EXECUTION_POLICY result;
HRESULT hr = WldpCanExecuteFile(
Copy link
Member

Choose a reason for hiding this comment

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

Are those calls synchronous right? It seems to block the event loop on every check making the system useless from a nodejs perspective

Copy link
Author

Choose a reason for hiding this comment

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

Yes. These calls are synchronous, and there are no async versions. Is there a way to address this without the platform adding an async version?

This call will only be made on systems that have opted into code integrity enforcement. I can reach out to the Windows platform team and see if there's opportunity to add an async version if this is an issue.

At the moment, is it acceptable to say there will be performance hits when using this feature?

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 will make this feature hard to use considering Node.js constraints, we can measure but I suspect it will make things quite slow

namespace codeintegrity {

#ifdef _WIN32
static bool isWldpInitialized = false;
Copy link
Member

Choose a reason for hiding this comment

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

We should avoid using global variables, and use a snapshottable API.

using v8::Object;
using v8::Value;

namespace codeintegrity {
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
namespace codeintegrity {
namespace code_integrity {

Comment on lines 233 to 236
static void IsInterpretiveModeDisabled(
const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(false);
}
Copy link
Member

Choose a reason for hiding this comment

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

There is no need for this function, since it only returns a constant boolean value.

Suggested change
static void IsInterpretiveModeDisabled(
const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(false);
}

@@ -34,6 +34,7 @@
"buffer": ["./lib/buffer.js"],
"child_process": ["./lib/child_process.js"],
"cluster": ["./lib/cluster.js"],
"codeintegrity": ["./lib/codeintegrity.js"],
Copy link
Member

Choose a reason for hiding this comment

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

we should also update the necessary files in typings folder for the internal APIs.

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.

  • It's missing some tests, for instance, eval (The check you placed inside internal/main/eval_string)
  • This feature should be opt-in, it can be either behind a flag or behind a compile time flag. To be honest, I'd first add it as a compile time flag, then we promote it to runtime flag.

@@ -198,6 +199,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.

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

Add calls to Windows Defender Application Control
to enforce integrity of .js, .json, .node files.
@RafaelGSS RafaelGSS added the request-ci Add this label to start a Jenkins CI on a PR. label Mar 4, 2025
@github-actions github-actions bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Mar 4, 2025
@nodejs-github-bot
Copy link
Collaborator

@rdw-msft rdw-msft requested a review from RafaelGSS March 5, 2025 21:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. 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.

8 participants