Skip to content

Commit 55b4cc9

Browse files
sinclairzx81homanp
andauthored
Version 1.1.37 (#1594)
* Prototype Pollution Guards on Value * ChangeLog * Version --------- Co-authored-by: homanp <homanp@gmail.com>
1 parent cf81396 commit 55b4cc9

12 files changed

Lines changed: 425 additions & 27 deletions

File tree

changelog/1.1.0.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
---
44

55
### Version Updates
6+
- [Revision 1.1.37](https://github.com/sinclairzx81/typebox/pull/1594)
7+
- Add Prototype Pollution Guards for Value.* Functions Clone, Diff, Patch, Mutate and Pointer.
68
- [Revision 1.1.36](https://github.com/sinclairzx81/typebox/pull/1592)
79
- Ensure no Type Distribution when Mapping for Min/Max Tuple Elements (JSON Schema)
810
- [Revision 1.1.35](https://github.com/sinclairzx81/typebox/pull/1591)

src/guard/guard.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,10 +189,13 @@ export function TakeLeft<T, True extends (left: T, right: T[]) => unknown, False
189189
// --------------------------------------------------------------------------
190190
// Object
191191
// --------------------------------------------------------------------------
192+
/** Returns true if the PropertyKey is Unsafe (ref: prototype-pollution). */
193+
export function IsUnsafePropertyKey(key: PropertyKey): boolean {
194+
return IsEqual(key, '__proto__') || IsEqual(key, 'constructor') || IsEqual(key, 'prototype')
195+
}
192196
/** Returns true if this value has this property key */
193197
export function HasPropertyKey<Key extends PropertyKey>(value: object, key: Key): value is { [_ in Key]: unknown } {
194-
const isProtoField = IsEqual(key, '__proto__') || IsEqual(key, 'constructor')
195-
return isProtoField ? Object.prototype.hasOwnProperty.call(value, key) : key in value
198+
return IsUnsafePropertyKey(key) ? Object.prototype.hasOwnProperty.call(value, key) : key in value
196199
}
197200
/** Returns object entries as `[RegExp, Value][]` */
198201
export function EntriesRegExp<Value extends unknown = unknown>(value: Record<PropertyKey, Value>): [RegExp, Value][] {
@@ -202,7 +205,7 @@ export function EntriesRegExp<Value extends unknown = unknown>(value: Record<Pro
202205
export function Entries<Value extends unknown = unknown>(value: Record<PropertyKey, Value>): [string, Value][] {
203206
return Object.entries(value)
204207
}
205-
/** Returns the property keys for this object via `Object.getOwnPropertyKeys({ ... })` */
208+
/** Returns property keys for this object via `Object.getOwnPropertyKeys({ ... })` */
206209
export function Keys(value: Record<PropertyKey, unknown>): string[] {
207210
return Object.getOwnPropertyNames(value)
208211
}

src/schema/pointer/pointer.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,17 @@ import { Guard } from '../../guard/index.ts'
3333
// ------------------------------------------------------------------
3434
// Asserts
3535
// ------------------------------------------------------------------
36-
function AssertNotRoot(indices: string[]) {
37-
if(indices.length === 0) throw Error('Cannot set root')
36+
function AssertNotRoot(indices: string[]): void {
37+
if (indices.length === 0) throw Error('Cannot set root')
3838
}
3939
function AssertCanSet(value: unknown): asserts value is Record<string, unknown> {
40-
if(!Guard.IsObject(value)) throw Error('Cannot set value')
40+
if (!Guard.IsObject(value)) throw Error('Cannot set value')
41+
}
42+
function AssertIndex(index: string): void {
43+
if (Guard.IsUnsafePropertyKey(index)) throw Error('Pointer contains unsafe property key')
44+
}
45+
function AssertIndices(indices: string[]): void {
46+
for (const index of indices) AssertIndex(index)
4147
}
4248
// ------------------------------------------------------------------
4349
// Indices
@@ -55,7 +61,7 @@ function HasIndex(index: string, value: unknown): value is Record<string, unknow
5561
return Guard.IsObject(value) && Guard.HasPropertyKey(value, index)
5662
}
5763
function GetIndex(index: string, value: unknown): unknown {
58-
return Guard.IsObject(value) ? value[index] : undefined
64+
return Guard.IsObject(value) && !Guard.IsUnsafePropertyKey(index) ? value[index] : undefined
5965
}
6066
function GetIndices(indices: string[], value: unknown): unknown {
6167
return indices.reduce((value, index) => GetIndex(index, value), value)
@@ -76,7 +82,7 @@ export function Indices(pointer: string): string[] {
7682
export function Has(value: unknown, pointer: string): unknown {
7783
let current = value
7884
return Indices(pointer).every(index => {
79-
if(!HasIndex(index, current)) return false
85+
if (!HasIndex(index, current)) return false
8086
current = current[index]
8187
return true
8288
})
@@ -96,6 +102,7 @@ export function Get(value: unknown, pointer: string): unknown {
96102
export function Set(value: unknown, pointer: string, next: unknown): unknown {
97103
const indices = Indices(pointer)
98104
AssertNotRoot(indices)
105+
AssertIndices(indices)
99106
const [head, index] = TakeIndexRight(indices)
100107
const parent = GetIndices(head, value)
101108
AssertCanSet(parent)
@@ -109,10 +116,11 @@ export function Set(value: unknown, pointer: string, next: unknown): unknown {
109116
export function Delete(value: unknown, pointer: string): unknown {
110117
const indices = Indices(pointer)
111118
AssertNotRoot(indices)
119+
AssertIndices(indices)
112120
const [head, index] = TakeIndexRight(indices)
113121
const parent = GetIndices(head, value)
114122
AssertCanSet(parent)
115-
if(Guard.IsArray(parent) && IsNumericIndex(index)) {
123+
if (Guard.IsArray(parent) && IsNumericIndex(index)) {
116124
parent.splice(+index, 1)
117125
} else {
118126
delete parent[index]

src/value/clone/clone.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,15 @@ function FromClassInstance(value: Record<PropertyKey, unknown>): Record<Property
4848
// ------------------------------------------------------------------
4949
function FromObjectInstance(value: Record<PropertyKey, unknown>): Record<PropertyKey, unknown> {
5050
const result = {} as Record<PropertyKey, unknown>
51-
for (const key of Object.getOwnPropertyNames(value)) {
51+
for (const key of Guard.Keys(value)) {
52+
if (Guard.IsUnsafePropertyKey(key)) continue // (ignore: prototype-pollution)
5253
result[key] = Clone(value[key])
5354
}
54-
for (const key of Object.getOwnPropertySymbols(value)) {
55+
for (const key of Guard.Symbols(value)) {
5556
result[key] = Clone(value[key])
5657
}
5758
return result
5859
}
59-
60-
Object.create({})
6160
// ------------------------------------------------------------------
6261
// Object
6362
// ------------------------------------------------------------------

src/value/delta/diff.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,15 @@ function* FromObject(path: string, left: Record<PropertyKey, unknown>, right: un
6666
// ----------------------------------------------------------------
6767
for (const key of rightKeys) {
6868
if (Guard.HasPropertyKey(left, key)) continue
69+
if (Guard.IsUnsafePropertyKey(key)) continue
6970
yield CreateInsert(`${path}/${key}`, right[key])
7071
}
7172
// ----------------------------------------------------------------
7273
// Update
7374
// ----------------------------------------------------------------
7475
for (const key of leftKeys) {
7576
if (!Guard.HasPropertyKey(right, key)) continue
77+
if (Guard.IsUnsafePropertyKey(key)) continue
7678
if (Equal(left, right)) continue
7779
yield* FromValue(`${path}/${key}`, left[key], right[key])
7880
}
@@ -81,6 +83,7 @@ function* FromObject(path: string, left: Record<PropertyKey, unknown>, right: un
8183
// ----------------------------------------------------------------
8284
for (const key of leftKeys) {
8385
if (Guard.HasPropertyKey(right, key)) continue
86+
if (Guard.IsUnsafePropertyKey(key)) continue
8487
yield CreateDelete(`${path}/${key}`)
8588
}
8689
}
@@ -107,10 +110,10 @@ function* FromArray(path: string, left: unknown[], right: unknown): IterableIter
107110
function* FromTypedArray(path: string, left: GlobalsGuard.TTypeArray, right: unknown): IterableIterator<TEdit> {
108111
const typeLeft = globalThis.Object.getPrototypeOf(left).constructor.name
109112
const typeRight = globalThis.Object.getPrototypeOf(right).constructor.name
110-
const predicate = GlobalsGuard.IsTypeArray(right)
113+
const predicate = GlobalsGuard.IsTypeArray(right)
111114
&& Guard.IsEqual(left.length, right.length)
112115
&& Guard.IsEqual(typeLeft, typeRight)
113-
if(predicate) {
116+
if (predicate) {
114117
for (let index = 0; index < Math.min(left.length, right.length); index++) {
115118
yield* FromValue(`${path}/${index}`, left[index], right[index])
116119
}
@@ -132,7 +135,7 @@ function* FromValue(path: string, left: unknown, right: unknown): IterableIterat
132135
return (
133136
GlobalsGuard.IsTypeArray(left) ? yield* FromTypedArray(path, left, right) :
134137
Guard.IsArray(left) ? yield* FromArray(path, left, right) :
135-
Guard.IsObject(left) ? yield* FromObject(path, left, right) :
138+
Guard.IsObject(left) ? yield* FromObject(path, left, right) :
136139
yield* FromUnknown(path, left, right)
137140
)
138141
}

src/value/mutate/from_object.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,23 +35,35 @@ import { Clone } from '../clone/index.ts'
3535
import { type TMutable } from './mutate.ts'
3636
import { FromValue } from './from_value.ts'
3737

38+
// ------------------------------------------------------------------
39+
// AssertKey
40+
// ------------------------------------------------------------------
41+
function AssertKey(key: string): void {
42+
if(Guard.IsUnsafePropertyKey(key)) throw Error('Attempted to Mutate with unsafe property key')
43+
}
44+
// ------------------------------------------------------------------
45+
// AssertKey
46+
// ------------------------------------------------------------------
3847
export function FromObject(root: TMutable, path: string, current: unknown, next: Record<string, unknown>): void {
3948
if (!Guard.IsObjectNotArray(current)) {
4049
Pointer.Set(root, path, Clone(next))
4150
} else {
4251
const currentKeys = Guard.Keys(current)
4352
const nextKeys = Guard.Keys(next)
4453
for (const currentKey of currentKeys) {
54+
AssertKey(currentKey)
4555
if (!nextKeys.includes(currentKey)) {
4656
delete current[currentKey]
4757
}
4858
}
4959
for (const nextKey of nextKeys) {
60+
AssertKey(nextKey)
5061
if (!currentKeys.includes(nextKey)) {
5162
current[nextKey] = next[nextKey]
5263
}
5364
}
5465
for (const nextKey of nextKeys) {
66+
AssertKey(nextKey)
5567
FromValue(root, `${path}/${nextKey}`, current[nextKey], next[nextKey])
5668
}
5769
}

tasks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Metrics } from './task/metrics/index.ts'
99
import { Spec } from './task/spec/index.ts'
1010
import { Task } from 'tasksmith'
1111

12-
const Version = '1.1.36'
12+
const Version = '1.1.37'
1313

1414
// ------------------------------------------------------------------
1515
// Build

test/typebox/runtime/schema/pointer/pointer.ts

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,9 @@ Test('Should Indices 20', () => {
8383
const R = [...Schema.Pointer.Indices('/x/a~1b///')]
8484
Assert.IsEqual(R, ['x', 'a/b', '', '', ''])
8585
})
86-
//-----------------------------------------------
86+
//-------------------------------------------------------------------
8787
// Get
88-
//-----------------------------------------------
88+
//-------------------------------------------------------------------
8989
Test('Should Get 1', () => {
9090
const V = [0, 1, 2, 3]
9191
Assert.IsEqual(Schema.Pointer.Get(V, ''), [0, 1, 2, 3])
@@ -108,9 +108,9 @@ Test('Should Get 2', () => {
108108
Assert.IsEqual(Schema.Pointer.Get(V, '/2/x'), 2)
109109
Assert.IsEqual(Schema.Pointer.Get(V, '/3/x'), 3)
110110
})
111-
//-----------------------------------------------
111+
//-------------------------------------------------------------------
112112
// Delete
113-
//-----------------------------------------------
113+
//-------------------------------------------------------------------
114114
Test('Should Delete 1', () => {
115115
const V = { x: {} }
116116
const R = Schema.Pointer.Delete(V, '/x/x')
@@ -131,9 +131,9 @@ Test('Should Delete 3', () => {
131131
const R = Schema.Pointer.Delete(V, '/100')
132132
Assert.IsEqual(R, [1, 2, 3])
133133
})
134-
//-----------------------------------------------
134+
//-------------------------------------------------------------------
135135
// Has
136-
//-----------------------------------------------
136+
//-------------------------------------------------------------------
137137
Test('Should Has 1', () => {
138138
const V: any = undefined
139139
Assert.IsEqual(Schema.Pointer.Has(V, ''), true)
@@ -154,9 +154,9 @@ Test('Should Has 5', () => {
154154
const V = { x: { y: {} } }
155155
Assert.IsEqual(Schema.Pointer.Has(V, '/x/y/z'), false)
156156
})
157-
//-----------------------------------------------
157+
//-------------------------------------------------------------------
158158
// Throw
159-
//-----------------------------------------------
159+
//-------------------------------------------------------------------
160160
Test('Should throw 1', () => {
161161
const V = {}
162162
Assert.Throws(() => Schema.Pointer.Set(V, '', { x: 1 }))
@@ -169,9 +169,9 @@ Test('Should throw 3', () => {
169169
const V = { x: 1 }
170170
Assert.Throws(() => Schema.Pointer.Set(V, '/x/y', 3))
171171
})
172-
//-----------------------------------------------
172+
//-------------------------------------------------------------------
173173
// Escapes
174-
//-----------------------------------------------
174+
//-------------------------------------------------------------------
175175
Test('Should support get ~0 Schema.Pointer escape', () => {
176176
const V = {
177177
x: { '~': { x: 1 } }
@@ -185,3 +185,51 @@ Test('Should support get ~1 Schema.Pointer escape', () => {
185185
}
186186
Assert.IsEqual(Schema.Pointer.Get(V, '/x/~1'), { x: 1 })
187187
})
188+
//-------------------------------------------------------------------
189+
// Unsafe Property Keys
190+
//-------------------------------------------------------------------
191+
Test('Should return undefined if attempting to Get an unsafe property key', () => {
192+
Assert.IsEqual(Schema.Pointer.Get({}, '/__proto__'), undefined)
193+
Assert.IsEqual(Schema.Pointer.Get({}, '/constructor'), undefined)
194+
Assert.IsEqual(Schema.Pointer.Get({}, '/prototype'), undefined)
195+
})
196+
Test('Should return undefined if attempting to Get a nested unsafe property key', () => {
197+
Assert.IsEqual(Schema.Pointer.Get({ a: {} }, '/a/__proto__'), undefined)
198+
Assert.IsEqual(Schema.Pointer.Get({ a: {} }, '/a/constructor'), undefined)
199+
Assert.IsEqual(Schema.Pointer.Get({ a: {} }, '/a/prototype'), undefined)
200+
})
201+
Test('Should return false if attempting to Has an unsafe property key', () => {
202+
Assert.IsEqual(Schema.Pointer.Has({}, '/__proto__'), false)
203+
Assert.IsEqual(Schema.Pointer.Has({}, '/constructor'), false)
204+
Assert.IsEqual(Schema.Pointer.Has({}, '/prototype'), false)
205+
})
206+
Test('Should return false if attempting to Has a nested unsafe property key', () => {
207+
Assert.IsEqual(Schema.Pointer.Has({ a: {} }, '/a/__proto__'), false)
208+
Assert.IsEqual(Schema.Pointer.Has({ a: {} }, '/a/constructor'), false)
209+
Assert.IsEqual(Schema.Pointer.Has({ a: {} }, '/a/prototype'), false)
210+
})
211+
Test('Should throw if attempting to Set an unsafe property key', () => {
212+
Assert.Throws(() => Schema.Pointer.Set({}, '/__proto__', 1))
213+
Assert.Throws(() => Schema.Pointer.Set({}, '/constructor', 1))
214+
Assert.Throws(() => Schema.Pointer.Set({}, '/prototype', 1))
215+
})
216+
Test('Should throw if attempting to Set a nested unsafe property key', () => {
217+
Assert.Throws(() => Schema.Pointer.Set({ a: {} }, '/a/__proto__', 1))
218+
Assert.Throws(() => Schema.Pointer.Set({ a: {} }, '/a/constructor', 1))
219+
Assert.Throws(() => Schema.Pointer.Set({ a: {} }, '/a/prototype', 1))
220+
})
221+
Test('Should throw if attempting to Delete an unsafe property key', () => {
222+
Assert.Throws(() => Schema.Pointer.Delete({}, '/__proto__'))
223+
Assert.Throws(() => Schema.Pointer.Delete({}, '/constructor'))
224+
Assert.Throws(() => Schema.Pointer.Delete({}, '/prototype'))
225+
})
226+
Test('Should throw if attempting to Delete a nested unsafe property key', () => {
227+
Assert.Throws(() => Schema.Pointer.Delete({ a: {} }, '/a/__proto__'))
228+
Assert.Throws(() => Schema.Pointer.Delete({ a: {} }, '/a/constructor'))
229+
Assert.Throws(() => Schema.Pointer.Delete({ a: {} }, '/a/prototype'))
230+
})
231+
Test('Should not throw for property names that contain but do not equal unsafe names', () => {
232+
Assert.IsEqual(Schema.Pointer.Get({ my_constructor: 1 }, '/my_constructor'), 1)
233+
Assert.IsEqual(Schema.Pointer.Get({ prototypes: 1 }, '/prototypes'), 1)
234+
Assert.IsEqual(Schema.Pointer.Get({ not__proto__: 1 }, '/not__proto__'), 1)
235+
})

test/typebox/runtime/value/clone/clone.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,41 @@ Test('Should Clone 12', () => {
7979
const B = Value.Clone(A)
8080
Assert.IsTrue(A === B)
8181
})
82+
// ----------------------------------------------------------------
83+
// Pollution Guards: Ensure No Unsafe Property is Cloned
84+
//
85+
// https://github.com/sinclairzx81/typebox/pull/1593
86+
// ----------------------------------------------------------------
87+
Test('Should Clone 13', () => {
88+
const A = { value: 1, constructor: 2 }
89+
const B = Value.Clone(A)
90+
Assert.IsEqual(B, { value: 1 })
91+
})
92+
Test('Should Clone 14', () => {
93+
const A = { value: 1, prototype: 2 }
94+
const B = Value.Clone(A)
95+
Assert.IsEqual(B, { value: 1 })
96+
})
97+
Test('Should Clone 15', () => {
98+
const A = { value: 1 }
99+
Object.defineProperty(A, '__proto__', { value: 2, enumerable: true })
100+
const B = Value.Clone(A)
101+
Assert.IsEqual(B, { value: 1 })
102+
})
103+
// Nested
104+
Test('Should Clone 16', () => {
105+
const A = { outer: { value: 1, constructor: 2 } }
106+
const B = Value.Clone(A)
107+
Assert.IsEqual(B, { outer: { value: 1 } })
108+
})
109+
Test('Should Clone 17', () => {
110+
const A = { outer: { value: 1, prototype: 2 } }
111+
const B = Value.Clone(A)
112+
Assert.IsEqual(B, { outer: { value: 1 } })
113+
})
114+
Test('Should Clone 18', () => {
115+
const A = { outer: { value: 1 } }
116+
Object.defineProperty(A.outer, '__proto__', { value: 2, enumerable: true })
117+
const B = Value.Clone(A)
118+
Assert.IsEqual(B, { outer: { value: 1 } })
119+
})

0 commit comments

Comments
 (0)