Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: disallow keys of JS Object prototype for safety #236

Merged
merged 1 commit into from
Jun 4, 2024
Merged
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
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
Loading