Skip to content

Commit

Permalink
Merge pull request #9720 from micalevisk/feat/repl-fix-injections
Browse files Browse the repository at this point in the history
fix(core): prevent renaming global providers and modules in the repl
  • Loading branch information
kamilmysliwiec committed Jun 15, 2022
2 parents 018fd6b + 1cc12ac commit 6843117
Show file tree
Hide file tree
Showing 12 changed files with 115 additions and 63 deletions.
5 changes: 1 addition & 4 deletions integration/repl/e2e/repl.spec.ts
Expand Up @@ -12,7 +12,6 @@ import {
import { expect } from 'chai';
import * as sinon from 'sinon';
import { AppModule } from '../src/app.module';
import { UsersModule } from '../src/users/users.module';

const PROMPT = '\u001b[1G\u001b[0J> \u001b[3G';

Expand All @@ -28,13 +27,11 @@ describe('REPL', () => {
});
afterEach(() => {
sinon.restore();
delete globalThis[AppModule.name];
delete globalThis[UsersModule.name];
});

it('get()', async () => {
const server = await repl(AppModule);

server.context
let outputText = '';
sinon.stub(process.stdout, 'write').callsFake(text => {
outputText += text;
Expand Down
14 changes: 14 additions & 0 deletions packages/core/repl/assign-to-object.util.ts
@@ -0,0 +1,14 @@
/**
* Similar to `Object.assign` but copying properties descriptors from `source`
* as well.
*/
export function assignToObject<T, U>(target: T, source: U): T & U {
Object.defineProperties(
target,
Object.keys(source).reduce((descriptors, key) => {
descriptors[key] = Object.getOwnPropertyDescriptor(source, key);
return descriptors;
}, Object.create(null)),
);
return target as T & U;
}
33 changes: 0 additions & 33 deletions packages/core/repl/load-native-functions-into-context.ts

This file was deleted.

85 changes: 62 additions & 23 deletions packages/core/repl/repl-context.ts
Expand Up @@ -11,6 +11,7 @@ import {
ResolveReplFn,
SelectReplFn,
} from './native-functions';
import { ReplFunction } from './repl-function';
import type { ReplFunctionClass } from './repl.interfaces';

type ModuleKey = string;
Expand All @@ -19,9 +20,12 @@ export type ModuleDebugEntry = {
providers: Record<string, InjectionToken>;
};

type ReplScope = Record<string, any>;

export class ReplContext {
public readonly logger = new Logger(ReplContext.name);
public debugRegistry: Record<ModuleKey, ModuleDebugEntry> = {};
public readonly globalScope: ReplScope = Object.create(null);
public readonly nativeFunctions = new Map<
string,
InstanceType<ReplFunctionClass>
Expand All @@ -33,6 +37,7 @@ export class ReplContext {
nativeFunctionsClassRefs?: ReplFunctionClass[],
) {
this.container = (app as any).container; // Using `any` because `app.container` is not public.

this.initializeContext();
this.initializeNativeFunctions(nativeFunctionsClassRefs || []);
}
Expand All @@ -41,40 +46,27 @@ export class ReplContext {
process.stdout.write(text);
}

public addNativeFunction(NativeFunction: ReplFunctionClass): void {
const nativeFunction = new NativeFunction(this);

this.nativeFunctions.set(nativeFunction.fnDefinition.name, nativeFunction);

nativeFunction.fnDefinition.aliases?.forEach(aliaseName => {
const aliasNativeFunction: InstanceType<ReplFunctionClass> =
Object.create(nativeFunction);
aliasNativeFunction.fnDefinition = {
name: aliaseName,
description: aliasNativeFunction.fnDefinition.description,
signature: aliasNativeFunction.fnDefinition.signature,
};
this.nativeFunctions.set(aliaseName, aliasNativeFunction);
});
}

private initializeContext() {
const globalRef = globalThis;
const modules = this.container.getModules();

modules.forEach(moduleRef => {
let moduleName = moduleRef.metatype.name;
if (moduleName === InternalCoreModule.name) {
return;
}
if (globalRef[moduleName]) {
if (this.globalScope[moduleName]) {
moduleName += ` (${moduleRef.token})`;
}

this.introspectCollection(moduleRef, moduleName, 'providers');
this.introspectCollection(moduleRef, moduleName, 'controllers');

globalRef[moduleName] = moduleRef.metatype;
// For in REPL auto-complete functionality
Object.defineProperty(this.globalScope, moduleName, {
value: moduleRef.metatype,
configurable: false,
enumerable: true,
});
});
}

Expand All @@ -88,12 +80,17 @@ export class ReplContext {
const stringifiedToken = this.stringifyToken(token);
if (
stringifiedToken === ApplicationConfig.name ||
stringifiedToken === moduleRef.metatype.name
stringifiedToken === moduleRef.metatype.name ||
this.globalScope[stringifiedToken]
) {
return;
}
// For in REPL auto-complete functionality
globalThis[stringifiedToken] = token;
Object.defineProperty(this.globalScope, stringifiedToken, {
value: token,
configurable: false,
enumerable: true,
});

if (stringifiedToken === ModuleRef.name) {
return;
Expand All @@ -115,6 +112,47 @@ export class ReplContext {
: token;
}

private addNativeFunction(
NativeFunctionRef: ReplFunctionClass,
): InstanceType<ReplFunctionClass> {
const nativeFunction = new NativeFunctionRef(this);

this.nativeFunctions.set(nativeFunction.fnDefinition.name, nativeFunction);

nativeFunction.fnDefinition.aliases?.forEach(aliaseName => {
const aliasNativeFunction: InstanceType<ReplFunctionClass> =
Object.create(nativeFunction);
aliasNativeFunction.fnDefinition = {
name: aliaseName,
description: aliasNativeFunction.fnDefinition.description,
signature: aliasNativeFunction.fnDefinition.signature,
};
this.nativeFunctions.set(aliaseName, aliasNativeFunction);
});

return nativeFunction;
}

private registerFunctionIntoGlobalScope(
nativeFunction: InstanceType<ReplFunctionClass>,
) {
// Bind the method to REPL's context:
this.globalScope[nativeFunction.fnDefinition.name] =
nativeFunction.action.bind(nativeFunction);

// Load the help trigger as a `help` getter on each native function:
const functionBoundRef: ReplFunction['action'] =
this.globalScope[nativeFunction.fnDefinition.name];
Object.defineProperty(functionBoundRef, 'help', {
enumerable: false,
configurable: false,
get: () =>
// Dynamically builds the help message as will unlikely to be called
// several times.
this.writeToStdout(nativeFunction.makeHelpMessage()),
});
}

private initializeNativeFunctions(
nativeFunctionsClassRefs: ReplFunctionClass[],
): void {
Expand All @@ -130,7 +168,8 @@ export class ReplContext {
builtInFunctionsClassRefs
.concat(nativeFunctionsClassRefs)
.forEach(NativeFunction => {
this.addNativeFunction(NativeFunction);
const nativeFunction = this.addNativeFunction(NativeFunction);
this.registerFunctionIntoGlobalScope(nativeFunction);
});
}
}
5 changes: 2 additions & 3 deletions packages/core/repl/repl.ts
Expand Up @@ -3,9 +3,9 @@ import * as _repl from 'repl';
import { clc } from '@nestjs/common/utils/cli-colors.util';
import { NestFactory } from '../nest-factory';
import { REPL_INITIALIZED_MESSAGE } from './constants';
import { loadNativeFunctionsIntoContext } from './load-native-functions-into-context';
import { ReplContext } from './repl-context';
import { ReplLogger } from './repl-logger';
import { assignToObject } from './assign-to-object.util';

export async function repl(module: Type) {
const app = await NestFactory.create(module, {
Expand All @@ -21,8 +21,7 @@ export async function repl(module: Type) {
prompt: clc.green('> '),
ignoreUndefined: true,
});

loadNativeFunctionsIntoContext(replServer.context, replContext);
assignToObject(replServer.context, replContext.globalScope);

return replServer;
}
36 changes: 36 additions & 0 deletions packages/core/test/repl/assign-to-object.util.spec.ts
@@ -0,0 +1,36 @@
import { expect } from 'chai';
import { assignToObject } from '@nestjs/core/repl/assign-to-object.util';

describe('assignToObject', () => {
it('should copy all enumerable properties and their descriptors', () => {
const sourceObj = {};
Object.defineProperty(sourceObj, 'foo', {
value: 123,
configurable: true,
enumerable: true,
writable: true,
});
Object.defineProperty(sourceObj, 'bar', {
value: 456,
configurable: true,
enumerable: true,
writable: false,
});
const targetObj = {};

assignToObject(targetObj, sourceObj);

expect(Object.getOwnPropertyDescriptor(targetObj, 'foo')).to.be.eql({
value: 123,
configurable: true,
enumerable: true,
writable: true,
});
expect(Object.getOwnPropertyDescriptor(targetObj, 'bar')).to.be.eql({
value: 456,
configurable: true,
enumerable: true,
writable: false,
});
});
});

0 comments on commit 6843117

Please sign in to comment.