Skip to content

Commit

Permalink
fix: disallow keys of JS Object prototype for safety
Browse files Browse the repository at this point in the history
  • Loading branch information
wschurman committed Jun 4, 2024
1 parent 4caea03 commit 5819e0d
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 78 deletions.
16 changes: 15 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,20 @@ 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.getPrototypeOf({}));
for (const disallowedFieldsKey of disallowedFieldsKeys) {
// when `hasOwnProperty` is a field name, we can't call it as a method. it's still invalid though.
if (typeof schema.hasOwnProperty !== 'function') {
throw new Error(`Entity field name not allowed: hasOwnProperty`);
}

if (schema.hasOwnProperty(disallowedFieldsKey)) {
throw new Error(`Entity field name not allowed: ${disallowedFieldsKey}`);
}
}
}

private static computeCacheableKeys<TFields>(
schema: ReadonlyMap<keyof TFields, EntityFieldDefinition<any>>
): ReadonlySet<keyof TFields> {
Expand Down
116 changes: 116 additions & 0 deletions packages/entity/src/__tests__/EntityConfiguration-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
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: ${keyName}`);
});
});
});
});
77 changes: 0 additions & 77 deletions packages/entity/src/__tests__/EntityDataConfiguration-test.ts

This file was deleted.

0 comments on commit 5819e0d

Please sign in to comment.