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

Ability to overwrite process.env. Ensure env changes don't leak. #9

Merged
merged 2 commits into from
Feb 27, 2024
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
28 changes: 15 additions & 13 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,33 @@ declare namespace fromMem {
*/
type FromMemOptions = {
/**
* What format does the code have? "guess" means to read the closest
* package.json file looking for the "type" key.
* What format does the code
* have? "guess" means to read the closest package.json file looking for
* the "type" key. "globals", "amd", and "bare" are not actually supported.
*/
format?: SourceFormat | undefined;
/**
* What is the fully-qualified synthetic
* filename for the code? Most important is the directory, which is used to
* find modules that the code import's or require's.
* If specified, use this instead of the
* current values in process.env. Works if includeGlobals is false by
* creating an otherwise-empty process instance.
*/
env?: Record<string, any> | undefined;
/**
* What is the fully-qualified synthetic filename
* for the code? Most important is the directory, which is used to find
* modules that the code import's or require's.
*/
filename: string;
/**
* Variables to make availble in the global
* scope while code is being evaluated.
* Variables to make availble in
* the global scope while code is being evaluated.
*/
context?: object | undefined;
context?: Record<string, any> | undefined;
/**
* Include the typical global
* properties that node gives to all modules. (e.g. Buffer, process).
*/
includeGlobals?: boolean | undefined;
/**
* For type "globals", what name is
* exported from the module?
*/
globalExport?: string | undefined;
/**
* Specifies the line number offset that is
* displayed in stack traces produced by this script.
Expand Down
62 changes: 41 additions & 21 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,27 @@ const vm = require("node:vm");

// These already exist in a new, blank VM. Date, JSON, NaN, etc.
// Things from the core language.
const vmGlobals = new vm
const vmGlobals = new Set(new vm
.Script("Object.getOwnPropertyNames(globalThis)")
.runInNewContext()
.sort();
vmGlobals.push("global", "globalThis", "sys");
.runInNewContext());
vmGlobals.add("global");
vmGlobals.add("globalThis");
vmGlobals.add("sys");

// These are the things that are normally in the environment, that vm doesn't
// make available. This that you expect to be available in a node environment
// that aren't in the laguage itself.
// that aren't in the laguage itself. There are a lot more things in this list
// than you expect, like setTimeout and structuredClone.
const neededKeys = Object
.getOwnPropertyNames(global)
.filter(k => !vmGlobals.includes(k))
.filter(k => !vmGlobals.has(k))
.sort();
const globalContext = Object.fromEntries(
neededKeys.map(k => [k, global[
/** @type {keyof typeof global} */ (k)
]])
);

// In node <15, console is in vmGlobals.
globalContext.console = console;

/**
Expand All @@ -56,18 +57,19 @@ globalContext.console = console;
* Options for how to process code.
*
* @typedef {object} FromMemOptions
* @property {SourceFormat} [format="commonjs"]
* What format does the code have? "guess" means to read the closest
* package.json file looking for the "type" key.
* @property {string} filename What is the fully-qualified synthetic
* filename for the code? Most important is the directory, which is used to
* find modules that the code import's or require's.
* @property {object} [context={}] Variables to make availble in the global
* scope while code is being evaluated.
* @property {SourceFormat} [format="commonjs"] What format does the code
* have? "guess" means to read the closest package.json file looking for
* the "type" key. "globals", "amd", and "bare" are not actually supported.
* @property {Record<string, any>} [env] If specified, use this instead of the
* current values in process.env. Works if includeGlobals is false by
* creating an otherwise-empty process instance.
* @property {string} filename What is the fully-qualified synthetic filename
* for the code? Most important is the directory, which is used to find
* modules that the code import's or require's.
* @property {Record<string, any>} [context={}] Variables to make availble in
* the global scope while code is being evaluated.
* @property {boolean} [includeGlobals=true] Include the typical global
* properties that node gives to all modules. (e.g. Buffer, process).
* @property {string} [globalExport=null] For type "globals", what name is
* exported from the module?
* @property {number} [lineOffset=0] Specifies the line number offset that is
* displayed in stack traces produced by this script.
* @property {number} [columnOffset=0] Specifies the first-line column number
Expand Down Expand Up @@ -248,9 +250,8 @@ guessModuleType.clearCache = function clearCache() {
async function fromMem(code, options) {
options = {
format: "commonjs",
context: {},
env: undefined,
includeGlobals: true,
globalExport: undefined,
lineOffset: 0,
columnOffset: 0,
...options,
Expand All @@ -261,11 +262,30 @@ async function fromMem(code, options) {
...globalContext,
...options.context,
};
} else {
// Put this here instead of in the defaults above so that typescript
// can see it.
options.context = options.context || {};
}

// Make sure env changes don't stick. This isn't a security measure, it's
// to prevent mistakes. There are probably a few other places where
// mistakes are likely, and the same treatment should be given.
if (options.context.process) {
if (options.context.process === process) {
options.context.process = { ...process };
}
options.context.process.env = options.env || {
...options.context.process.env,
};
} else if (options.env) {
options.context.process = {
version: process.version,
env: { ...options.env },
};
}

// @ts-expect-error Context is always non-null
options.context.global = options.context;
// @ts-expect-error Context is always non-null
options.context.globalThis = options.context;

if (!options.filename) {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"semver": "7.6.0"
},
"devDependencies": {
"@peggyjs/eslint-config": "3.2.3",
"@peggyjs/eslint-config": "3.2.4",
"@types/node": "20.11.20",
"@types/semver": "7.5.8",
"c8": "9.1.0",
Expand Down
8 changes: 4 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 36 additions & 0 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,39 @@ test("no SourceTextModule", async() => {
// Reset
vm.SourceTextModule = stm;
});

test("process.env", async() => {
// No process gives the right error
await assert.rejects(() => fromMem("module.exports = process", {
filename: join(__dirname, "test11.js"),
format: "cjs",
includeGlobals: false,
}), /process is not defined/);

// Pick up current value
process.env.___TEST1___ = "12";
assert.equal((await fromMem("module.exports = process.env.___TEST1___", {
filename: join(__dirname, "test12.js"),
format: "cjs",
})), "12");
delete process.env.___TEST1___;

// Anti-pollution
assert.equal((await fromMem(`
process.env.___TEST2___ = "13";
module.exports = process.env.___TEST2___`, {
filename: join(__dirname, "test13.js"),
format: "cjs",
})), "13");
assert.equal(typeof process.env.___TEST2___, "undefined");

// Fake process
assert.equal((await fromMem("module.exports = process.env.___TEST3___", {
filename: join(__dirname, "test14.js"),
format: "cjs",
includeGlobals: false,
env: {
___TEST3___: "14",
},
})), "14");
});
Loading