Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/lint-release-proposal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ jobs:
gh api \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
--jq '.commits.[] | { smallSha: .sha[0:10] } + (.commit.message|capture("^(?<title>.+)\n\n(.*\n)*PR-URL: (?<prURL>.+)\n"))' \
--jq '.commits.[] | { smallSha: .sha[0:10] } + (.commit.message|capture("^(?<title>.+)\n\n(.*\n)*PR-URL: (?<prURL>.+)(\n|$)"))' \
"/repos/${GITHUB_REPOSITORY}/compare/v${MAJOR}.x...$GITHUB_SHA" --paginate \
| node tools/actions/lint-release-proposal-commit-list.mjs "$CHANGELOG_PATH" "$GITHUB_SHA" \
| while IFS= read -r PR_URL; do
Expand Down
64 changes: 61 additions & 3 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,9 +320,17 @@ the community they pose.
* Avoid exposing low-level or dangerous APIs directly to untrusted users.

* Examples of scenarios that are **not** Node.js vulnerabilities:
* Allowing untrusted users to register SQLite user-defined functions that can
perform arbitrary operations (e.g., closing database connections during query
execution, causing crashes or use-after-free conditions).
* Allowing untrusted users to register SQLite user-defined functions via
`node:sqlite` (`DatabaseSync`) that can perform arbitrary operations
(e.g., closing database connections during query execution, causing crashes
or use-after-free conditions).
* Loading SQLite extensions using the `allowExtension` option in
`DatabaseSync` — this option must be explicitly set to `true` by the
application, and enabling it is the application operator's responsibility.
* Using `node:sqlite` built-in SQL functions or pragmas (e.g.,
`ATTACH DATABASE`) to read or write files — `DatabaseSync` operates with
the same file-system access as the process itself, and it is the
application's responsibility to restrict what SQL is executed.
* Exposing `child_process.exec()` or similar APIs to untrusted users without
proper input validation, allowing command injection.
* Allowing untrusted users to control file paths passed to file system APIs
Expand Down Expand Up @@ -362,6 +370,56 @@ the community they pose.
responsibility to properly handle errors by attaching appropriate
`'error'` event listeners to EventEmitters that may emit errors.

#### Permission Model Boundaries (`--permission`)

The Node.js [Permission Model](https://nodejs.org/api/permissions.html)
(`--experimental-permission`) is an opt-in mechanism that limits which
resources a Node.js process may access. It is designed to reduce the blast
radius of mistakes in trusted application code, **not** to act as a security
boundary against intentional misuse or a compromised process.

The following are **not** vulnerabilities in Node.js:

* **Operator-controlled flags**: Behavior unlocked by flags the operator
explicitly passes (e.g., `--localstorage-file`) is the operator's
responsibility. The permission model does not restrict how Node.js behaves
when the operator intentionally configures it.

* **`node:sqlite` and the permission model**: `DatabaseSync` operates with the
same file-system privileges as the process. Using SQL pragmas or built-in
SQLite mechanisms (e.g., `ATTACH DATABASE`) to access files does not bypass
the permission model — the permission model does not intercept SQL-level
file operations.

* **Path resolution and symlinks**: `fs.realpathSync()`, `fs.realpath()`, and
similar functions resolve a path to its canonical form before the permission
check is applied. Accessing a file through a symlink that resolves to an
allowed path is the intended behavior, not a bypass. TOCTOU races on
symlinks that resolve within the allowed list are similarly not considered
permission model bypasses.

* **`worker_threads` with modified `execArgv`**: Workers inherit the permission
restrictions of their parent process. Passing an empty or modified `execArgv`
to a worker does not grant it additional permissions.

#### V8 Sandbox

The V8 sandbox is an in-process isolation mechanism internal to V8 that is not
a Node.js security boundary. Node.js does not guarantee or document the V8
sandbox as a security feature, and it is not enabled in a way that provides
security guarantees in production Node.js builds. Reports about escaping the V8
sandbox are not considered Node.js vulnerabilities; they should be reported
directly to the [V8 project](https://v8.dev/docs/security-bugs).

#### CRLF Injection in `writeEarlyHints()`

`ServerResponse.writeEarlyHints()` accepts a `link` header value that is set
by the application. Passing arbitrary strings, including CRLF sequences, as
the `link` value is an application-level misuse of the API, not a Node.js
vulnerability. Node.js validates the structure of Early Hints per the HTTP spec
but does not sanitize free-form application data passed to it; that is the
application's responsibility.

## Assessing experimental features reports

Experimental features are eligible for security reports just like any other
Expand Down
14 changes: 14 additions & 0 deletions deps/merve/BUILD.gn
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
##############################################################################
# #
# DO NOT EDIT THIS FILE! #
# #
##############################################################################

# This file is used by GN for building, which is NOT the build system used for
# building official binaries.
# Please modify the gyp files if you are making changes to build system.

import("unofficial.gni")

merve_gn_build("merve") {
}
20 changes: 20 additions & 0 deletions deps/merve/unofficial.gni
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# This file is used by GN for building, which is NOT the build system used for
# building official binaries.
# Please edit the gyp files if you are making changes to build system.

# The actual configurations are put inside a template in unofficial.gni to
# prevent accidental edits from contributors.
template("merve_gn_build") {
config("merve_config") {
include_dirs = [ "." ]
}
gypi_values = exec_script("../../tools/gypi_to_gn.py",
[ rebase_path("merve.gyp") ],
"scope",
[ "merve.gyp" ])
source_set(target_name) {
forward_variables_from(invoker, "*")
public_configs = [ ":merve_config" ]
sources = gypi_values.merve_sources
}
}
20 changes: 19 additions & 1 deletion lib/_http_client.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const {
Error,
NumberIsFinite,
ObjectAssign,
ObjectDefineProperty,
ObjectKeys,
ObjectSetPrototypeOf,
ReflectApply,
Expand Down Expand Up @@ -116,6 +117,7 @@ let debug = require('internal/util/debuglog').debuglog('http', (fn) => {

const INVALID_PATH_REGEX = /[^\u0021-\u00ff]/;
const kError = Symbol('kError');
const kPath = Symbol('kPath');

const kLenientAll = HTTPParser.kLenientAll | 0;
const kLenientNone = HTTPParser.kLenientNone | 0;
Expand Down Expand Up @@ -303,7 +305,7 @@ function ClientRequest(input, options, cb) {

this.joinDuplicateHeaders = options.joinDuplicateHeaders;

this.path = options.path || '/';
this[kPath] = options.path || '/';
if (cb) {
this.once('response', cb);
}
Expand Down Expand Up @@ -446,6 +448,22 @@ function ClientRequest(input, options, cb) {
ObjectSetPrototypeOf(ClientRequest.prototype, OutgoingMessage.prototype);
ObjectSetPrototypeOf(ClientRequest, OutgoingMessage);

ObjectDefineProperty(ClientRequest.prototype, 'path', {
__proto__: null,
get() {
return this[kPath];
},
set(value) {
const path = String(value);
if (INVALID_PATH_REGEX.test(path)) {
throw new ERR_UNESCAPED_CHARACTERS('Request path');
}
this[kPath] = path;
},
configurable: true,
enumerable: true,
});

ClientRequest.prototype._finish = function _finish() {
OutgoingMessage.prototype._finish.call(this);
if (hasObserver('http')) {
Expand Down
11 changes: 10 additions & 1 deletion lib/_http_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ const {
kUniqueHeaders,
parseUniqueHeadersOption,
OutgoingMessage,
validateHeaderName,
validateHeaderValue,
} = require('_http_outgoing');
const {
kOutHeaders,
Expand Down Expand Up @@ -333,13 +335,20 @@ ServerResponse.prototype.writeEarlyHints = function writeEarlyHints(hints, cb) {
return;
}

if (checkInvalidHeaderChar(link)) {
throw new ERR_INVALID_CHAR('header content', 'Link');
}

head += 'Link: ' + link + '\r\n';

const keys = ObjectKeys(hints);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (key !== 'link') {
head += key + ': ' + hints[key] + '\r\n';
validateHeaderName(key);
const value = hints[key];
validateHeaderValue(key, value);
head += key + ': ' + value + '\r\n';
}
}

Expand Down
100 changes: 60 additions & 40 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -1034,41 +1034,63 @@ function getExportsForCircularRequire(module) {
return module.exports;
}


/**
* Resolve a module request for CommonJS, invoking hooks from module.registerHooks()
* if necessary.
* Wraps result of Module._resolveFilename to include additional fields for hooks.
* See resolveForCJSWithHooks.
* @param {string} specifier
* @param {Module|undefined} parent
* @param {boolean} isMain
* @param {boolean} shouldSkipModuleHooks
* @param {ResolveFilenameOptions} options
* @returns {{url?: string, format?: string, parentURL?: string, filename: string}}
*/
function resolveForCJSWithHooks(specifier, parent, isMain, shouldSkipModuleHooks) {
let defaultResolvedURL;
let defaultResolvedFilename;
let format;

function defaultResolveImpl(specifier, parent, isMain, options) {
// For backwards compatibility, when encountering requests starting with node:,
// throw ERR_UNKNOWN_BUILTIN_MODULE on failure or return the normalized ID on success
// without going into Module._resolveFilename.
let normalized;
if (StringPrototypeStartsWith(specifier, 'node:')) {
normalized = BuiltinModule.normalizeRequirableId(specifier);
if (!normalized) {
throw new ERR_UNKNOWN_BUILTIN_MODULE(specifier);
}
defaultResolvedURL = specifier;
format = 'builtin';
return normalized;
function wrapResolveFilename(specifier, parent, isMain, options) {
const filename = Module._resolveFilename(specifier, parent, isMain, options).toString();
return { __proto__: null, url: undefined, format: undefined, filename };
}

/**
* See resolveForCJSWithHooks.
* @param {string} specifier
* @param {Module|undefined} parent
* @param {boolean} isMain
* @param {ResolveFilenameOptions} options
* @returns {{url?: string, format?: string, parentURL?: string, filename: string}}
*/
function defaultResolveImplForCJSLoading(specifier, parent, isMain, options) {
// For backwards compatibility, when encountering requests starting with node:,
// throw ERR_UNKNOWN_BUILTIN_MODULE on failure or return the normalized ID on success
// without going into Module._resolveFilename.
let normalized;
if (StringPrototypeStartsWith(specifier, 'node:')) {
normalized = BuiltinModule.normalizeRequirableId(specifier);
if (!normalized) {
throw new ERR_UNKNOWN_BUILTIN_MODULE(specifier);
}
return Module._resolveFilename(specifier, parent, isMain, options).toString();
return { __proto__: null, url: specifier, format: 'builtin', filename: normalized };
}
return wrapResolveFilename(specifier, parent, isMain, options);
}

/**
* Resolve a module request for CommonJS, invoking hooks from module.registerHooks()
* if necessary.
* @param {string} specifier
* @param {Module|undefined} parent
* @param {boolean} isMain
* @param {object} internalResolveOptions
* @param {boolean} internalResolveOptions.shouldSkipModuleHooks Whether to skip module hooks.
* @param {ResolveFilenameOptions} internalResolveOptions.requireResolveOptions Options from require.resolve().
* Only used when it comes from require.resolve().
* @returns {{url?: string, format?: string, parentURL?: string, filename: string}}
*/
function resolveForCJSWithHooks(specifier, parent, isMain, internalResolveOptions) {
const { requireResolveOptions, shouldSkipModuleHooks } = internalResolveOptions;
const defaultResolveImpl = requireResolveOptions ?
wrapResolveFilename : defaultResolveImplForCJSLoading;
// Fast path: no hooks, just return simple results.
if (!resolveHooks.length || shouldSkipModuleHooks) {
const filename = defaultResolveImpl(specifier, parent, isMain);
return { __proto__: null, url: defaultResolvedURL, filename, format };
return defaultResolveImpl(specifier, parent, isMain, requireResolveOptions);
}

// Slow path: has hooks, do the URL conversions and invoke hooks with contexts.
Expand Down Expand Up @@ -1097,27 +1119,25 @@ function resolveForCJSWithHooks(specifier, parent, isMain, shouldSkipModuleHooks
} else {
conditionSet = getCjsConditions();
}
defaultResolvedFilename = defaultResolveImpl(specifier, parent, isMain, {

const result = defaultResolveImpl(specifier, parent, isMain, {
__proto__: null,
paths: requireResolveOptions?.paths,
conditions: conditionSet,
});
// If the default resolver does not return a URL, convert it for the public API.
result.url ??= convertCJSFilenameToURL(result.filename);

defaultResolvedURL = convertCJSFilenameToURL(defaultResolvedFilename);
return { __proto__: null, url: defaultResolvedURL };
// Remove filename because it's not part of the public API.
// TODO(joyeecheung): maybe expose it in the public API to avoid re-conversion for users too.
return { __proto__: null, url: result.url, format: result.format };
}

const resolveResult = resolveWithHooks(specifier, parentURL, /* importAttributes */ undefined,
getCjsConditionsArray(), defaultResolve);
const { url } = resolveResult;
format = resolveResult.format;

let filename;
if (url === defaultResolvedURL) { // Not overridden, skip the re-conversion.
filename = defaultResolvedFilename;
} else {
filename = convertURLToCJSFilename(url);
}

const { url, format } = resolveResult;
// Convert the URL from the hook chain back to a filename for internal use.
const filename = convertURLToCJSFilename(url);
const result = { __proto__: null, url, format, filename, parentURL };
debug('resolveForCJSWithHooks', specifier, parent?.id, isMain, shouldSkipModuleHooks, '->', result);
return result;
Expand Down Expand Up @@ -1212,10 +1232,10 @@ function loadBuiltinWithHooks(id, url, format) {
* @param {string} request Specifier of module to load via `require`
* @param {Module} parent Absolute path of the module importing the child
* @param {boolean} isMain Whether the module is the main entry point
* @param {object|undefined} options Additional options for loading the module
* @param {object|undefined} internalResolveOptions Additional options for loading the module
* @returns {object}
*/
Module._load = function(request, parent, isMain, options = kEmptyObject) {
Module._load = function(request, parent, isMain, internalResolveOptions = kEmptyObject) {
let relResolveCacheIdentifier;
if (parent) {
debug('Module._load REQUEST %s parent: %s', request, parent.id);
Expand All @@ -1238,7 +1258,7 @@ Module._load = function(request, parent, isMain, options = kEmptyObject) {
}
}

const resolveResult = resolveForCJSWithHooks(request, parent, isMain, options.shouldSkipModuleHooks);
const resolveResult = resolveForCJSWithHooks(request, parent, isMain, internalResolveOptions);
let { format } = resolveResult;
const { url, filename } = resolveResult;

Expand Down
7 changes: 5 additions & 2 deletions lib/internal/modules/esm/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -722,13 +722,16 @@ class ModuleLoader {
* `module.registerHooks()` hooks.
* @param {string} [parentURL] See {@link resolve}.
* @param {ModuleRequest} request See {@link resolve}.
* @param {boolean} [shouldSkipSyncHooks] Whether to skip the synchronous hooks registered by module.registerHooks().
* This is used to maintain compatibility for the re-invented require.resolve (in imported CJS customized
* by module.register()`) which invokes the CJS resolution separately from the hook chain.
* @returns {{ format: string, url: string }}
*/
resolveSync(parentURL, request) {
resolveSync(parentURL, request, shouldSkipSyncHooks = false) {
const specifier = `${request.specifier}`;
const importAttributes = request.attributes ?? kEmptyObject;

if (syncResolveHooks.length) {
if (!shouldSkipSyncHooks && syncResolveHooks.length) {
// Has module.registerHooks() hooks, chain the asynchronous hooks in the default step.
return resolveWithSyncHooks(specifier, parentURL, importAttributes, this.#defaultConditions,
this.#resolveAndMaybeBlockOnLoaderThread.bind(this));
Expand Down
Loading
Loading