Skip to content
Open
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
18 changes: 17 additions & 1 deletion src/node_modules.cc
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,23 @@ const BindingData::PackageConfig* BindingData::GetPackageJSON(
PackageConfig package_config{};
package_config.file_path = path;
// No need to exclude BOM since simdjson will skip it.
if (ReadFileSync(&package_config.raw_json, path.data()) < 0) {
int read_error = ReadFileSync(&package_config.raw_json, path.data());
if (read_error < 0) {
// No file at this path, a path component that is not a directory, or a
// "package.json" that is itself a directory all mean there is no package
// config here. Any other failure means a package.json is present but
// could not be read. Treating that as absent silently drops fields such
// as "exports" and "type", which can resolve a specifier to a different
// file, so surface the read error instead of continuing.
if (read_error != UV_ENOENT && read_error != UV_ENOTDIR &&
read_error != UV_EISDIR) {
THROW_ERR_INVALID_PACKAGE_CONFIG(realm->isolate(),
"Cannot read package config %s: %s.",
path.data(),
uv_strerror(read_error));
return nullptr;
}

// Add `nullopt` to the package config cache so that we don't
// need to open and attempt to read this path again
binding_data->package_configs_.insert({std::string(path), std::nullopt});
Expand Down
57 changes: 57 additions & 0 deletions test/parallel/test-module-unreadable-package-json.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'use strict';

// A package.json that exists but cannot be read must not be treated as
// absent. Doing so silently drops fields such as "exports", which can resolve
// a specifier to a different file than the one the package declares.
// Refs: https://github.com/nodejs/node/issues/65220

const common = require('../common');

if (common.isWindows) {
common.skip('chmod does not restrict reads on Windows');
}
if (process.getuid?.() === 0) {
common.skip('cannot make a file unreadable as root');
}

const assert = require('assert');
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const tmpdir = require('../common/tmpdir');

tmpdir.refresh();

const depDir = tmpdir.resolve('node_modules/dep');
fs.mkdirSync(path.join(depDir, 'lib'), { recursive: true });
const depPackageJson = path.join(depDir, 'package.json');
fs.writeFileSync(
depPackageJson,
'{"name":"dep","exports":{".":"./lib/real.js"}}',
);
fs.writeFileSync(path.join(depDir, 'lib', 'real.js'), 'export const which = "real";');
// If the package config is ignored, resolution falls back to this file.
fs.writeFileSync(path.join(depDir, 'index.js'), 'export const which = "decoy";');

fs.writeFileSync(tmpdir.resolve('package.json'), '{"type":"module"}');
const entry = tmpdir.resolve('main.mjs');
fs.writeFileSync(entry, 'import { which } from "dep"; console.log(which);');

// Sanity check: the export resolves while the package config is readable.
{
const child = spawnSync(process.execPath, [entry], { encoding: 'utf8' });
assert.strictEqual(child.status, 0, child.stderr);
assert.strictEqual(child.stdout.trim(), 'real');
}

fs.chmodSync(depPackageJson, 0o000);

{
const child = spawnSync(process.execPath, [entry], { encoding: 'utf8' });
// The read failure must be reported rather than resolving to index.js.
assert.notStrictEqual(child.status, 0);
assert.doesNotMatch(child.stdout, /decoy/);
assert.match(child.stderr, /Cannot read package config/);
}

fs.chmodSync(depPackageJson, 0o644);
Loading