CVE-2020-7748: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') (CWE-1321)
value: This affects the package @tsed/core before 5.65.7. This vulnerability relates to the deepExtend function which is used as part of the utils directory. Depending on if user input is provided, an attacker can overwrite and pollute the object prototype of a program.
CWE description: The product receives input from an upstream component that specifies attributes that are to be initialized or updated in an object, but it does not properly control modifications of attributes of the object prototype.
CVSS v3 base score: 8.1
Published: 2020-10-20T11:15Z
Repository: https://github.com/TypedProject/tsed
Language: TypeScript
Files changed: packages/common/src/converters/components/MapConverter.ts, packages/common/src/converters/components/SetConverter.ts, packages/common/src/converters/services/ConverterService.ts, packages/core/src/utils/cleanObject.ts, packages/json-mapper/src/components/MapMapper.ts, packages/json-mapper/src/components/SetMapper.ts, packages/json-mapper/src/utils/deserialize.ts, packages/mongoose/src/utils/createSchema.ts
Commit date: 2020-10-18 08:28:05 +0200
Commit message: fix: Use objectKeys to prevent prototype pollution

Diff: diff --git a/packages/common/src/converters/components/MapConverter.ts b/packages/common/src/converters/components/MapConverter.ts
index ff0c53849..92f055679 100644
--- a/packages/common/src/converters/components/MapConverter.ts
+++ b/packages/common/src/converters/components/MapConverter.ts
@@ -1,5 +1,6 @@
+import {objectKeys} from "@tsed/core";
 import {Converter} from "../decorators/converter";
 import {IConverter, IDeserializer, ISerializer} from "../interfaces/index";
 
 /**
  * Converter component for the `Map` Type.
@@ -17,11 +18,11 @@ export class MapConverter implements IConverter {
    * @returns {Map<string, T>}
    */
   deserialize<T>(data: any, target: any, baseType: T, deserializer: IDeserializer): Map<string, T> {
     const obj = new Map<string, T>();
 
-    Object.keys(data).forEach((key) => {
+    objectKeys(data).forEach((key) => {
       obj.set(key, deserializer(data[key], baseType) as T);
     });
 
     return obj;
   }
diff --git a/packages/common/src/converters/components/SetConverter.ts b/packages/common/src/converters/components/SetConverter.ts
index 2befba758..ff53e9c51 100644
--- a/packages/common/src/converters/components/SetConverter.ts
+++ b/packages/common/src/converters/components/SetConverter.ts
@@ -1,5 +1,6 @@
+import {objectKeys} from "@tsed/core";
 import {Converter} from "../decorators/converter";
 import {IConverter, IDeserializer, ISerializer} from "../interfaces/index";
 
 /**
  * Converter component for the `Set` Type.
@@ -17,11 +18,11 @@ export class SetConverter implements IConverter {
    * @returns {Map<string, T>}
    */
   deserialize<T>(data: any, target: any, baseType: T, deserializer: IDeserializer): Set<T> {
     const obj = new Set<T>();
 
-    Object.keys(data).forEach((key) => {
+    objectKeys(data).forEach((key) => {
       obj.add(deserializer(data[key], baseType) as T);
     });
 
     return obj;
   }
diff --git a/packages/common/src/converters/services/ConverterService.ts b/packages/common/src/converters/services/ConverterService.ts
index b8f97ceaa..3be7e237b 100644
--- a/packages/common/src/converters/services/ConverterService.ts
+++ b/packages/common/src/converters/services/ConverterService.ts
@@ -1,6 +1,6 @@
-import {getClass, isArrayOrArrayClass, isEmpty, isPrimitiveOrPrimitiveClass, Metadata, Type} from "@tsed/core";
+import {getClass, isArrayOrArrayClass, isEmpty, isPrimitiveOrPrimitiveClass, Metadata, objectKeys, Type} from "@tsed/core";
 import {Configuration, Injectable, InjectorService} from "@tsed/di";
 import {IConverterSettings} from "../../config/interfaces/IConverterSettings";
 import {PropertyMetadata} from "../../mvc/models/PropertyMetadata";
 import {getJsonSchema} from "../../jsonschema/utils/getJsonSchema";
 import {ArrayConverter, DateConverter, MapConverter, PrimitiveConverter, SetConverter, SymbolConverter} from "../components";
@@ -99,11 +99,11 @@ export class ConverterService {
   serializeClass(obj: any, options: IConverterOptions = {}) {
     const {checkRequiredValue = true, withIgnoredProps} = options;
 
     const plainObject: any = {};
     const properties = PropertyMetadata.getProperties(options.type || obj, {withIgnoredProps});
-    const keys = properties.size ? Array.from(properties.keys()) : Object.keys(obj);
+    const keys = properties.size ? Array.from(properties.keys()) : objectKeys(obj);
 
     keys.forEach((propertyKey) => {
       if (typeof obj[propertyKey] !== "function") {
         let propertyMetadata = ConverterService.getPropertyMetadata(properties, propertyKey);
         let propertyValue = obj[propertyKey];
@@ -177,11 +177,11 @@ export class ConverterService {
 
     // Default converter
     const instance = new targetType();
     const properties = PropertyMetadata.getProperties(targetType);
 
-    Object.keys(obj).forEach((propertyName: string) => {
+    objectKeys(obj).forEach((propertyName: string) => {
       const propertyMetadata = ConverterService.getPropertyMetadata(properties, propertyName);
 
       return this.convertProperty(obj, instance, propertyName, propertyMetadata, options);
     });
 
diff --git a/packages/core/src/utils/cleanObject.ts b/packages/core/src/utils/cleanObject.ts
index ab1521eff..f1b89f577 100644
--- a/packages/core/src/utils/cleanObject.ts
+++ b/packages/core/src/utils/cleanObject.ts
@@ -1,17 +1,19 @@
+import {isProtectedKey} from "./isProtectedKey";
 /**
  * Remove undefined value
  * @param obj
  */
-
 export function cleanObject(obj: any): any {
-  return Object.entries(obj).reduce(
-    (obj, [key, value]) =>
-      value === undefined
-        ? obj
-        : {
-            ...obj,
-            [key]: value
-          },
-    {}
-  );
+  return Object.entries(obj).reduce((obj, [key, value]) => {
+    if (isProtectedKey(key)) {
+      return obj;
+    }
+
+    return value === undefined
+      ? obj
+      : {
+          ...obj,
+          [key]: value
+        };
+  }, {});
 }
diff --git a/packages/json-mapper/src/components/MapMapper.ts b/packages/json-mapper/src/components/MapMapper.ts
index ca82d11c3..2b7602ff8 100644
--- a/packages/json-mapper/src/components/MapMapper.ts
+++ b/packages/json-mapper/src/components/MapMapper.ts
@@ -1,5 +1,6 @@
+import {objectKeys} from "@tsed/core";
 import {JsonMapper} from "../decorators/jsonMapper";
 import {JsonMapperCtx, JsonMapperMethods} from "../interfaces/JsonMapperMethods";
 
 /**
  * Converter component for the `Map` Type.
@@ -10,11 +11,11 @@ import {JsonMapperCtx, JsonMapperMethods} from "../interfaces/JsonMapperMethods"
 @JsonMapper(Map)
 export class MapMapper implements JsonMapperMethods {
   deserialize<T = any, C = Map<string, T>>(data: {[key: string]: any}, ctx: JsonMapperCtx<T, C>): Map<string, T> {
     const obj = new Map<string, T>();
 
-    Object.keys(data).forEach((key) => {
+    objectKeys(data).forEach((key) => {
       obj.set(key, ctx.next(data[key]) as T);
     });
 
     return obj;
   }
diff --git a/packages/json-mapper/src/components/SetMapper.ts b/packages/json-mapper/src/components/SetMapper.ts
index 5c0e47669..10e2fe812 100644
--- a/packages/json-mapper/src/components/SetMapper.ts
+++ b/packages/json-mapper/src/components/SetMapper.ts
@@ -1,5 +1,6 @@
+import {objectKeys} from "@tsed/core";
 import {JsonMapper} from "../decorators/jsonMapper";
 import {JsonMapperCtx, JsonMapperMethods} from "../interfaces/JsonMapperMethods";
 
 /**
  * Converter component for the `Set` Type.
@@ -10,11 +11,11 @@ import {JsonMapperCtx, JsonMapperMethods} from "../interfaces/JsonMapperMethods"
 @JsonMapper(Set)
 export class SetMapper implements JsonMapperMethods {
   deserialize<T>(data: any, ctx: JsonMapperCtx): Set<T> {
     const obj = new Set<T>();
 
-    Object.keys(data).forEach((key) => {
+    objectKeys(data).forEach((key) => {
       obj.add(ctx.next(data[key]));
     });
 
     return obj;
   }
diff --git a/packages/json-mapper/src/utils/deserialize.ts b/packages/json-mapper/src/utils/deserialize.ts
index 53f6e8da8..62e151152 100644
--- a/packages/json-mapper/src/utils/deserialize.ts
+++ b/packages/json-mapper/src/utils/deserialize.ts
@@ -1,6 +1,6 @@
-import {isArray, isEmpty, isNil, MetadataTypes, nameOf, Type} from "@tsed/core";
+import {isArray, isEmpty, isNil, MetadataTypes, nameOf, objectKeys, Type} from "@tsed/core";
 import {getPropertiesStores, JsonEntityStore, JsonHookContext, JsonSchema} from "@tsed/schema";
 import "../components";
 import {JsonMapperContext} from "../domain/JsonMapperContext";
 import {getJsonMapperTypes} from "../domain/JsonMapperTypesContainer";
 import {JsonMapperMethods} from "../interfaces/JsonMapperMethods";
@@ -75,11 +75,11 @@ export function plainObjectToClass<T = any>(src: any, options: JsonDeserializerO
   }
 
   const {type, store = JsonEntityStore.from(type), ...next} = options;
   const propertiesMap = getPropertiesStores(store);
 
-  let keys = Object.keys(src);
+  let keys = objectKeys(src);
   const additionalProperties = propertiesMap.size ? !!store.schema.get("additionalProperties") || options.additionalProperties : true;
   const out: any = new type(src);
 
   propertiesMap.forEach((propStore) => {
     const key = propStore.parent.schema.getAliasOf(propStore.propertyName) || propStore.propertyName;
diff --git a/packages/mongoose/src/utils/cleanProps.ts b/packages/mongoose/src/utils/cleanProps.ts
deleted file mode 100644
index ae920e194..000000000
--- a/packages/mongoose/src/utils/cleanProps.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export const cleanProps = (src: any) =>
-  Object.keys(src).reduce((obj: any, k: any) => {
-    if (src[k] !== undefined) {
-      obj[k] = src[k];
-    }
-
-    return obj;
-  }, {});
diff --git a/packages/mongoose/src/utils/createSchema.ts b/packages/mongoose/src/utils/createSchema.ts
index 5aaf69905..2bab73548 100644
--- a/packages/mongoose/src/utils/createSchema.ts
+++ b/packages/mongoose/src/utils/createSchema.ts
@@ -1,12 +1,11 @@
 import {ConverterService, IConverterOptions, JsonSchema, PropertyMetadata} from "@tsed/common";
-import {getClass, Store, Type} from "@tsed/core";
+import {cleanObject, getClass, Store, Type} from "@tsed/core";
 import * as mongoose from "mongoose";
 import {SchemaDefinition, SchemaTypeOpts} from "mongoose";
 import {MONGOOSE_SCHEMA} from "../constants";
 import {MongooseSchemaOptions} from "../interfaces";
-import {cleanProps} from "./cleanProps";
 import {schemaOptions} from "./schemaOptions";
 
 const MONGOOSE_RESERVED_KEYS = ["_id"];
 
 export interface MongooseSchemaMetadata {
@@ -130,11 +129,11 @@ export function createSchemaTypeOptions(propertyMetadata: PropertyMetadata): Sch
   } else if (!rawMongooseSchema.ref) {
     // References are handled by the final merge
     schemaTypeOptions = {...schemaTypeOptions, type: getSchema(propertyMetadata.type)};
   }
 
-  schemaTypeOptions = cleanProps({...schemaTypeOptions, ...rawMongooseSchema});
+  schemaTypeOptions = cleanObject({...schemaTypeOptions, ...rawMongooseSchema});
 
   if (propertyMetadata.isCollection) {
     if (propertyMetadata.isArray) {
       schemaTypeOptions = [schemaTypeOptions];
     } else {

Vulnerable code:     Object.keys(data).forEach((key) => {
    Object.keys(data).forEach((key) => {
import {getClass, isArrayOrArrayClass, isEmpty, isPrimitiveOrPrimitiveClass, Metadata, Type} from "@tsed/core";
    const keys = properties.size ? Array.from(properties.keys()) : Object.keys(obj);
    Object.keys(obj).forEach((propertyName: string) => {

  return Object.entries(obj).reduce(
    (obj, [key, value]) =>
      value === undefined
        ? obj
        : {
            ...obj,
            [key]: value
          },
    {}
  );
    Object.keys(data).forEach((key) => {
    Object.keys(data).forEach((key) => {
import {isArray, isEmpty, isNil, MetadataTypes, nameOf, Type} from "@tsed/core";
  let keys = Object.keys(src);
export const cleanProps = (src: any) =>
  Object.keys(src).reduce((obj: any, k: any) => {
    if (src[k] !== undefined) {
      obj[k] = src[k];
    }

    return obj;
  }, {});
import {getClass, Store, Type} from "@tsed/core";
import {cleanProps} from "./cleanProps";
  schemaTypeOptions = cleanProps({...schemaTypeOptions, ...rawMongooseSchema});
Fixed code: import {objectKeys} from "@tsed/core";
    objectKeys(data).forEach((key) => {
import {objectKeys} from "@tsed/core";
    objectKeys(data).forEach((key) => {
import {getClass, isArrayOrArrayClass, isEmpty, isPrimitiveOrPrimitiveClass, Metadata, objectKeys, Type} from "@tsed/core";
    const keys = properties.size ? Array.from(properties.keys()) : objectKeys(obj);
    objectKeys(obj).forEach((propertyName: string) => {
import {isProtectedKey} from "./isProtectedKey";
  return Object.entries(obj).reduce((obj, [key, value]) => {
    if (isProtectedKey(key)) {
      return obj;
    }

    return value === undefined
      ? obj
      : {
          ...obj,
          [key]: value
        };
  }, {});
import {objectKeys} from "@tsed/core";
    objectKeys(data).forEach((key) => {
import {objectKeys} from "@tsed/core";
    objectKeys(data).forEach((key) => {
import {isArray, isEmpty, isNil, MetadataTypes, nameOf, objectKeys, Type} from "@tsed/core";
  let keys = objectKeys(src);
import {cleanObject, getClass, Store, Type} from "@tsed/core";
  schemaTypeOptions = cleanObject({...schemaTypeOptions, ...rawMongooseSchema});