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
10 changes: 10 additions & 0 deletions __tests__/cjs/import.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,16 @@ export default code
}
}
})

it('should isolate prototype modifications', async () => {
const originalToString = Object.prototype.toString

await importFromStringFn('Object.prototype.toString = () => "hacked"; export default true')

expect(Object.prototype.toString).toBe(originalToString)
// eslint-disable-next-line @typescript-eslint/no-base-to-string
expect({}.toString()).not.toBe('hacked')
})
}

describe('importFromString', () => {
Expand Down
25 changes: 25 additions & 0 deletions __tests__/cjs/require.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,28 @@ it('should use absolute filename in error stack trace', () => {
}
}
})

it('should isolate global variables', () => {
global.testVar = 'original'

requireFromString('global.testVar = "modified"')

expect(global.testVar).toBe('original')
delete global.testVar
})

it('should isolate prototype modifications', () => {
const originalToString = Object.prototype.toString

requireFromString('Object.prototype.toString = () => "hacked"')

expect(Object.prototype.toString).toBe(originalToString)
// eslint-disable-next-line @typescript-eslint/no-base-to-string
expect({}.toString()).not.toBe('hacked')
})

it('should have isolated Error constructor by default', () => {
const error = requireFromString('module.exports = new Error("test")')
expect(error instanceof Error).toBe(false)
expect(error.constructor.name).toBe('Error')
})
10 changes: 10 additions & 0 deletions __tests__/esm/import.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,16 @@ export default code
}
}
})

it('should isolate prototype modifications', async () => {
const originalToString = Object.prototype.toString

await importFromString('Object.prototype.toString = () => "hacked"; export default true')

expect(Object.prototype.toString).toBe(originalToString)
// eslint-disable-next-line @typescript-eslint/no-base-to-string
expect({}.toString()).not.toBe('hacked')
})
})

describe('importFromStringSync', () => {
Expand Down
4 changes: 2 additions & 2 deletions src/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,13 @@ Enable '--experimental-vm-modules' CLI option or replace it with dynamic 'import
const moduleFilename = getModuleFilename(dirname, filename)
const moduleFileURLString = ensureFileURL(moduleFilename)

const globalObject = createGlobalObject(globals, useCurrentGlobal)
const globalMap = createGlobalObject(globals, useCurrentGlobal)
const contextObject = createContextObject(
{
__dirname: ensurePath(dirname),
__filename: ensurePath(moduleFilename)
},
globalObject
globalMap
)
contextObject[IMPORTS] = {}
const context = createContext(contextObject)
Expand Down
4 changes: 2 additions & 2 deletions src/require.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const requireFromString = (
contextModule.filename = moduleFilename
contextModule.paths = mainModule?.paths ?? []

const globalObject = createGlobalObject(globals, useCurrentGlobal)
const globalMap = createGlobalObject(globals, useCurrentGlobal)
const contextObject = createContextObject(
{
exports: contextModule.exports,
Expand All @@ -44,7 +44,7 @@ export const requireFromString = (
__filename: contextModule.filename,
__dirname: contextModule.path
},
globalObject
globalMap
)

runInNewContext(code, contextObject, {
Expand Down
57 changes: 37 additions & 20 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,29 +88,46 @@ const getCurrentGlobal = (): Context => {
return currentGlobal
}

export const createGlobalObject = (globals: Context, useCurrentGlobal: boolean): Context => {
const globalObject = useCurrentGlobal
? getCurrentGlobal()
: Object.defineProperty({}, Symbol.toStringTag, {
...Object.getOwnPropertyDescriptor(__GLOBAL__, Symbol.toStringTag)
})
forEachPropertyKey(globals, propertyKey => {
if (propertyKey in __GLOBAL__) {
Object.defineProperty(globalObject, propertyKey, {
...Object.getOwnPropertyDescriptor(__GLOBAL__, propertyKey),
value: globals[propertyKey as keyof Context]
})
} else {
Object.defineProperty(globalObject, propertyKey, {
...Object.getOwnPropertyDescriptor(globals, propertyKey)
})
}
export const createGlobalObject = (
globals: Context,
useCurrentGlobal: boolean
): Map<string | symbol, any> => {
const globalMap = new Map()

if (useCurrentGlobal) {
const currentGlobal = getCurrentGlobal()
Object.getOwnPropertyNames(currentGlobal).forEach(key => {
globalMap.set(key, currentGlobal[key])
})
Object.getOwnPropertySymbols(currentGlobal).forEach(key => {
// @ts-expect-error: safe to ignore
globalMap.set(key, currentGlobal[key])
})
}

// Add user globals to Map (protected from pollution)
Object.getOwnPropertyNames(globals).forEach(key => {
globalMap.set(key, globals[key])
})
return globalObject
Object.getOwnPropertySymbols(globals).forEach(key => {
// @ts-expect-error: safe to ignore
globalMap.set(key, globals[key])
})

return globalMap
}

export const createContextObject = (moduleContext: Context, globalObject: Context): Context => {
const contextObject: Context = shallowMergeContext(moduleContext, globalObject)
export const createContextObject = (
moduleContext: Context,
globalMap: Map<string | symbol, any>
): Context => {
const contextObject: Context = { ...moduleContext }

// Convert Map back to object for VM context, but only with safe values
globalMap.forEach((value, key) => {
contextObject[key as keyof Context] = value
})

if (!('global' in contextObject)) {
contextObject.global = contextObject
}
Expand Down