Skip to content

Commit

Permalink
fix: disallow keys of JS Object prototype for safety (#236)
Browse files Browse the repository at this point in the history
  • Loading branch information
wschurman authored Jun 4, 2024
1 parent 7e2cea1 commit 05726d4
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 88 deletions.
13 changes: 12 additions & 1 deletion packages/entity/src/EntityConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export default class EntityConfiguration<TFields extends Record<string, any>> {

// external schema is a Record to typecheck that all fields have FieldDefinitions,
// but internally the most useful representation is a map for lookups
// TODO(wschurman): validate schema
EntityConfiguration.validateSchema(schema);
this.schema = new Map(Object.entries(schema));

this.cacheableKeys = EntityConfiguration.computeCacheableKeys(this.schema);
Expand All @@ -85,6 +85,17 @@ export default class EntityConfiguration<TFields extends Record<string, any>> {
this.dbToEntityFieldsKeyMapping = invertMap(this.entityToDBFieldsKeyMapping);
}

private static validateSchema<TFields extends Record<string, any>>(schema: TFields): void {
const disallowedFieldsKeys = Object.getOwnPropertyNames(Object.prototype);
for (const disallowedFieldsKey of disallowedFieldsKeys) {
if (Object.hasOwn(schema, disallowedFieldsKey)) {
throw new Error(
`Entity field name not allowed to prevent conflicts with standard Object prototype fields: ${disallowedFieldsKey}`
);
}
}
}

private static computeCacheableKeys<TFields>(
schema: ReadonlyMap<keyof TFields, EntityFieldDefinition<any>>
): ReadonlySet<keyof TFields> {
Expand Down
118 changes: 118 additions & 0 deletions packages/entity/src/__tests__/EntityConfiguration-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import EntityConfiguration from '../EntityConfiguration';
import { UUIDField, StringField } from '../EntityFields';

describe(EntityConfiguration, () => {
describe('when valid', () => {
type BlahT = {
id: string;
cacheable: string;
uniqueButNotCacheable: string;
};

type Blah2T = {
id: string;
};

const blahEntityConfiguration = new EntityConfiguration<BlahT>({
idField: 'id',
tableName: 'blah_table',
schema: {
id: new UUIDField({
columnName: 'id',
}),
cacheable: new StringField({
columnName: 'cacheable',
cache: true,
}),
uniqueButNotCacheable: new StringField({
columnName: 'unique_but_not_cacheable',
}),
},
databaseAdapterFlavor: 'postgres',
cacheAdapterFlavor: 'redis',
});

it('returns correct fields', () => {
expect(blahEntityConfiguration.idField).toEqual('id');
expect(blahEntityConfiguration.tableName).toEqual('blah_table');
expect(blahEntityConfiguration.databaseAdapterFlavor).toEqual('postgres');
expect(blahEntityConfiguration.cacheAdapterFlavor).toEqual('redis');
});

it('filters cacheable fields', () => {
expect(blahEntityConfiguration.cacheableKeys).toEqual(new Set(['cacheable']));
});

describe('cache key version', () => {
it('defaults to 0', () => {
const entityConfiguration = new EntityConfiguration<Blah2T>({
idField: 'id',
tableName: 'blah',
schema: {
id: new UUIDField({
columnName: 'id',
}),
},
databaseAdapterFlavor: 'postgres',
cacheAdapterFlavor: 'redis',
});
expect(entityConfiguration.cacheKeyVersion).toEqual(0);
});

it('sets to custom version', () => {
const entityConfiguration = new EntityConfiguration<Blah2T>({
idField: 'id',
tableName: 'blah',
schema: {
id: new UUIDField({
columnName: 'id',
}),
},
databaseAdapterFlavor: 'postgres',
cacheAdapterFlavor: 'redis',
cacheKeyVersion: 100,
});
expect(entityConfiguration.cacheKeyVersion).toEqual(100);
});
});
});

describe('validation', () => {
describe('disallows keys of JS Object prototype for safety', () => {
test.each([
'constructor',
'__defineGetter__',
'__defineSetter__',
'hasOwnProperty',
'__lookupGetter__',
'__lookupSetter__',
'isPrototypeOf',
'propertyIsEnumerable',
'toString',
'valueOf',
'__proto__',
'toLocaleString',
])('disallows %p as field key', (keyName) => {
expect(
() =>
new EntityConfiguration<any>({
idField: 'id',
tableName: 'blah_table',
schema: {
id: new UUIDField({
columnName: 'id',
}),
[keyName]: new StringField({
columnName: 'any',
}),
},
databaseAdapterFlavor: 'postgres',
cacheAdapterFlavor: 'redis',
})
).toThrow(
`Entity field name not allowed to prevent conflicts with standard Object prototype fields: ${keyName}`
);
});
});
});
});
77 changes: 0 additions & 77 deletions packages/entity/src/__tests__/EntityDataConfiguration-test.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/entity/src/errors/EntityError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default abstract class EntityError extends ES6Error {
public abstract readonly state: EntityErrorState;
public abstract readonly code: EntityErrorCode;

constructor(message: string, public readonly cause?: Error) {
constructor(message: string, public override readonly cause?: Error) {
super(message);
}
}
4 changes: 2 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "es2021",
"lib": ["es2021"],
"target": "es2022",
"lib": ["es2023"],
"module": "commonjs",
"sourceMap": true,
"moduleResolution": "node",
Expand Down
20 changes: 13 additions & 7 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -675,37 +675,38 @@
integrity sha512-reebgVwjf8VfZxSXU7e+UjpXGwcUTIMpWR9FY54Oh70ulhXrQiZei62B4D9bH3SVYMwnDGzifHJ8INRrJ+0L1g==

"@expo/entity-cache-adapter-local-memory@file:packages/entity-cache-adapter-local-memory":
version "0.31.1"
version "0.35.0"
dependencies:
lru-cache "^6.0.0"

"@expo/entity-cache-adapter-redis@file:packages/entity-cache-adapter-redis":
version "0.31.1"
version "0.35.0"

"@expo/entity-database-adapter-knex@file:packages/entity-database-adapter-knex":
version "0.31.1"
version "0.35.0"
dependencies:
knex "^2.4.2"

"@expo/entity-ip-address-field@file:packages/entity-ip-address-field":
version "0.31.1"
version "0.35.0"
dependencies:
ip-address "^8.1.0"

"@expo/entity-secondary-cache-local-memory@file:packages/entity-secondary-cache-local-memory":
version "0.31.1"
version "0.35.0"

"@expo/entity-secondary-cache-redis@file:packages/entity-secondary-cache-redis":
version "0.31.1"
version "0.35.0"

"@expo/entity@file:packages/entity":
version "0.31.1"
version "0.35.0"
dependencies:
"@expo/results" "^1.0.0"
dataloader "^2.0.0"
es6-error "^4.1.1"
invariant "^2.2.4"
uuid "^8.3.0"
uuidv7 "^1.0.0"

"@expo/results@^1.0.0":
version "1.0.0"
Expand Down Expand Up @@ -8661,6 +8662,11 @@ uuid@^9.0.0:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5"
integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==

uuidv7@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/uuidv7/-/uuidv7-1.0.0.tgz#b097dd0d48c5e48edf661199e033f10ebee08cda"
integrity sha512-XkvPwTtSmYwxIE1FSYQTYg79zHL1ZWV5vM/Qyl9ahXCU8enOPPA4bTjzvafvYUB7l2+miv4EqK/qEe75cOXIdA==

v8-compile-cache@2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
Expand Down

0 comments on commit 05726d4

Please sign in to comment.