From 1ce4ed60e9588c48e702f895187e903cc47b576d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Vanvelthem?= Date: Fri, 17 May 2024 19:50:59 +0200 Subject: [PATCH 1/3] fix(assert): isPlainObject allows Object.create(null) --- .changeset/yellow-bottles-smell.md | 5 +++++ packages/assert/src/object.guards.ts | 14 ++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 .changeset/yellow-bottles-smell.md diff --git a/.changeset/yellow-bottles-smell.md b/.changeset/yellow-bottles-smell.md new file mode 100644 index 000000000..e0ae4e570 --- /dev/null +++ b/.changeset/yellow-bottles-smell.md @@ -0,0 +1,5 @@ +--- +"@httpx/assert": patch +--- + +isPlainObject allows Object.create(null) and disallow stringTagName and iterators symbols diff --git a/packages/assert/src/object.guards.ts b/packages/assert/src/object.guards.ts index 243b55495..70014d2ef 100644 --- a/packages/assert/src/object.guards.ts +++ b/packages/assert/src/object.guards.ts @@ -5,10 +5,16 @@ export const isPlainObject = < >( v: unknown ): v is PlainObject => { + if (v === null || typeof v !== 'object') { + return false; + } + const proto = Object.getPrototypeOf(v) as typeof Object.prototype | null; return ( - typeof v === 'object' && - v !== null && - (Object.getPrototypeOf(v) as typeof Object.prototype)?.constructor === - Object.prototype.constructor + (proto === null || + proto === Object.prototype || + Object.getPrototypeOf(proto) === null) && + // https://stackoverflow.com/a/76387885/5490184 + !(Symbol.toStringTag in v) && + !(Symbol.iterator in v) ); }; From d1d7c8ed8868e9041e6d5be167d4e6f884b313bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Vanvelthem?= Date: Fri, 17 May 2024 19:51:22 +0200 Subject: [PATCH 2/3] fix(assert): isPlainObject allows Object.create(null) --- .../src/__tests__/object.guards.test.ts | 51 +++++++++++++++---- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/packages/assert/src/__tests__/object.guards.test.ts b/packages/assert/src/__tests__/object.guards.test.ts index da7aa6ab8..6cd32c276 100644 --- a/packages/assert/src/__tests__/object.guards.test.ts +++ b/packages/assert/src/__tests__/object.guards.test.ts @@ -2,10 +2,14 @@ import { isPlainObject } from '../object.guards'; describe('Object typeguards tests', () => { describe('isPlainObject', () => { + const str = 'key'; it.each([ [{}, true], + [Object.create(null), true], [{ 1: 'cool' }, true], [{ name: 'seb' }, true], + [{ [str]: 'seb' }, true], + [{ [Symbol('tag')]: 'value' }, true], [{ children: [{ test: 1 }], name: 'deep-plain' }, true], [ { children: [{ test: new Date() }], name: 'deep-with-regular-object' }, @@ -13,24 +17,53 @@ describe('Object typeguards tests', () => { ], [{ constructor: { name: 'Object2' } }, true], [JSON.parse('{}'), true], - // False + // ############ Rejected ############################# + ['hello', false], + [false, false], + [undefined, false], + [null, false], + [10, false], + [Number.NaN, false], + // functions and objects [() => 'cool', false], [new (class Cls {})(), false], [new (class extends Object {})(), false], + // Symbols + [Symbol('cool'), false], + [ + { + [Symbol.iterator]: function* () { + yield 1; + }, + }, + false, + ], + [ + { + [Symbol.toStringTag]: 'string-tagged', + }, + false, + ], + // Static built-in classes + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON + [JSON, false], + [Math, false], + [Atomics, false], + // built-in classes [new Date(), false], [new Map(), false], [new Error(), false], [new Set(), false], [new Request('http://localhost'), false], - [Object.create(null), false], - [/(\d+)/, false], [new Promise(() => {}), false], - ['hello', false], - [false, false], - [undefined, false], - [null, false], - [10, false], - [Number.NaN, false], + [Promise.resolve({}), false], + [Object.create({}), false], + [/(\d+)/, false], + // eslint-disable-next-line prefer-regex-literals + [new RegExp('/d+/'), false], + // Template literals + [`cool`, false], + [String.raw`rawtemplate`, false], ])('when "%s" is given, should return %s', (v, expected) => { expect(isPlainObject(v)).toStrictEqual(expected); }); From c7ce0ceb3853795fc23c6ffaae10facb849962c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Vanvelthem?= Date: Fri, 17 May 2024 19:57:29 +0200 Subject: [PATCH 3/3] fix(assert): isPlainObject allows Object.create(null) --- docs/src/pages/assert/index.mdx | 7 +++++-- packages/assert/.size-limit.cjs | 6 +++--- packages/assert/README.md | 7 ++++--- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/docs/src/pages/assert/index.mdx b/docs/src/pages/assert/index.mdx index 98a21436a..0401face6 100644 --- a/docs/src/pages/assert/index.mdx +++ b/docs/src/pages/assert/index.mdx @@ -321,6 +321,7 @@ assertParsableStrictIsoDateZ('2023-02-29T23:37:31.653z'); // 👉 throws cause n | isUuidV3 | `string` | `UuidV3` | | | isUuidV4 | `string` | `UuidV4` | | | isUuidV5 | `string` | `UuidV5` | | +| isUuidV7 | `string` | `UuidV7` | | | assertUuid | `string` | `UuidV1 \| UuidV3 \| UuidV4 \| UuidV5 \| UuidV7` | | | assertUuidV1 | `string` | `UuidV5` | | | assertUuidV3 | `string` | `UuidV3` | | @@ -418,12 +419,14 @@ for the browser. ESM individual imports are tracked by a [size-limit configuration](https://github.com/belgattitude/httpx/blob/main/packages/assert/.size-limit.cjs). + | Scenario | Size (compressed) | |----------------------------------------|------------------:| -| Import `isPlainObject` | ~ 56b | +| Import `isPlainObject` | ~ 100b | | Import `isUuid` | ~ 175b | | Import `isEan13` | ~ 117b | -| All typeguards, assertions and helpers | ~ 900b | +| All typeguards, assertions and helpers | ~ 1700b | + > For CJS usage (not recommended) track the size on [bundlephobia](https://bundlephobia.com/package/@httpx/assert@latest). diff --git a/packages/assert/.size-limit.cjs b/packages/assert/.size-limit.cjs index 005620699..f1822e22d 100644 --- a/packages/assert/.size-limit.cjs +++ b/packages/assert/.size-limit.cjs @@ -13,7 +13,7 @@ module.exports = [ name: 'Only isPlainObject (ESM)', path: ['dist/index.mjs'], import: "{ isPlainObject }", - limit: "76B", + limit: "108B", }, { name: 'Only isUuid (ESM)', @@ -31,12 +31,12 @@ module.exports = [ name: 'Only assertPlainObject (ESM)', path: ['dist/index.mjs'], import: "{ assertPlainObject }", - limit: "461B", + limit: "487B", }, { name: 'Everything (CJS)', import: "*", path: ['dist/index.cjs'], - limit: '2400B', + limit: '2600B', }, ]; diff --git a/packages/assert/README.md b/packages/assert/README.md index a8f756881..2e56c545e 100644 --- a/packages/assert/README.md +++ b/packages/assert/README.md @@ -298,11 +298,12 @@ assertParsableStrictIsoDateZ('2023-02-29T23:37:31.653z'); // 👉 throws cause n | Name | Type | Opaque type | Comment | |----------------|-------------------------|--------------------------------------------------|--------| -| isUuid | `string` | `UuidV1 \| UuidV3 \| UuidV4 \| UuidV5 \| UuidV7` | | +| isUuid | `string` | `UuidV1 \| UuidV3 \| UuidV4 \| UuidV5 \| UuidV7` | | | isUuidV1 | `string` | `UuidV1` | | | isUuidV3 | `string` | `UuidV3` | | | isUuidV4 | `string` | `UuidV4` | | | isUuidV5 | `string` | `UuidV5` | | +| isUuidV7 | `string` | `UuidV7` | | | assertUuid | `string` | `UuidV1 \| UuidV3 \| UuidV4 \| UuidV5 \| UuidV7` | | | assertUuidV1 | `string` | `UuidV5` | | | assertUuidV3 | `string` | `UuidV3` | | @@ -401,10 +402,10 @@ ESM individual imports are tracked by a | Scenario | Size (compressed) | |----------------------------------------|------------------:| -| Import `isPlainObject` | ~ 56b | +| Import `isPlainObject` | ~ 100b | | Import `isUuid` | ~ 175b | | Import `isEan13` | ~ 117b | -| All typeguards, assertions and helpers | ~ 900b | +| All typeguards, assertions and helpers | ~ 1700b | > For CJS usage (not recommended) track the size on [bundlephobia](https://bundlephobia.com/package/@httpx/assert@latest).