Skip to content

Commit 65e3ee4

Browse files
committed
Prototype Pollution Guards on Value
1 parent cf81396 commit 65e3ee4

9 files changed

Lines changed: 364 additions & 18 deletions

File tree

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 appears unsafe (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: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,14 @@ 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')
4144
}
4245
// ------------------------------------------------------------------
4346
// Indices
@@ -55,7 +58,7 @@ function HasIndex(index: string, value: unknown): value is Record<string, unknow
5558
return Guard.IsObject(value) && Guard.HasPropertyKey(value, index)
5659
}
5760
function GetIndex(index: string, value: unknown): unknown {
58-
return Guard.IsObject(value) ? value[index] : undefined
61+
return Guard.IsObject(value) && !Guard.IsUnsafePropertyKey(index) ? value[index] : undefined
5962
}
6063
function GetIndices(indices: string[], value: unknown): unknown {
6164
return indices.reduce((value, index) => GetIndex(index, value), value)
@@ -76,7 +79,7 @@ export function Indices(pointer: string): string[] {
7679
export function Has(value: unknown, pointer: string): unknown {
7780
let current = value
7881
return Indices(pointer).every(index => {
79-
if(!HasIndex(index, current)) return false
82+
if (!HasIndex(index, current)) return false
8083
current = current[index]
8184
return true
8285
})
@@ -97,6 +100,7 @@ export function Set(value: unknown, pointer: string, next: unknown): unknown {
97100
const indices = Indices(pointer)
98101
AssertNotRoot(indices)
99102
const [head, index] = TakeIndexRight(indices)
103+
AssertIndex(index)
100104
const parent = GetIndices(head, value)
101105
AssertCanSet(parent)
102106
parent[index] = next
@@ -110,9 +114,10 @@ export function Delete(value: unknown, pointer: string): unknown {
110114
const indices = Indices(pointer)
111115
AssertNotRoot(indices)
112116
const [head, index] = TakeIndexRight(indices)
117+
AssertIndex(index)
113118
const parent = GetIndices(head, value)
114119
AssertCanSet(parent)
115-
if(Guard.IsArray(parent) && IsNumericIndex(index)) {
120+
if (Guard.IsArray(parent) && IsNumericIndex(index)) {
116121
parent.splice(+index, 1)
117122
} else {
118123
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
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: 8 additions & 5 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
}
@@ -131,9 +134,9 @@ function* FromUnknown(path: string, left: unknown, right: unknown): IterableIter
131134
function* FromValue(path: string, left: unknown, right: unknown): IterableIterator<TEdit> {
132135
return (
133136
GlobalsGuard.IsTypeArray(left) ? yield* FromTypedArray(path, left, right) :
134-
Guard.IsArray(left) ? yield* FromArray(path, left, right) :
135-
Guard.IsObject(left) ? yield* FromObject(path, left, right) :
136-
yield* FromUnknown(path, left, right)
137+
Guard.IsArray(left) ? yield* FromArray(path, left, right) :
138+
Guard.IsObject(left) ? yield* FromObject(path, left, right) :
139+
yield* FromUnknown(path, left, right)
137140
)
138141
}
139142
// ------------------------------------------------------------------

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
}

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+
})

test/typebox/runtime/value/delta/diff.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,3 +394,61 @@ Test('Should generate no diff for undefined properties of current and next', ()
394394
const E = [] as any
395395
Assert.IsEqual(D, E)
396396
})
397+
// ----------------------------------------------------------------
398+
// Pollution Guards: Ensure Unsafe Properties produce no Edits
399+
//
400+
// https://github.com/sinclairzx81/typebox/pull/1593
401+
// ----------------------------------------------------------------
402+
Test('Should not generate edits for unsafe properties on INSERT', () => {
403+
const A = {}
404+
const B = {
405+
value: 1,
406+
'__proto__': 1,
407+
'constructor': 1,
408+
'prototype': 1
409+
}
410+
const C = Value.Diff(A, B)
411+
Assert.IsEqual(C, [{ type: 'insert', path: '/value', value: 1 }])
412+
})
413+
Test('Should not generate edits for unsafe properties on UPDATE', () => {
414+
const A = {
415+
value: 1,
416+
'__proto__': 1,
417+
'constructor': 1,
418+
'prototype': 1
419+
}
420+
const B = {
421+
value: 2,
422+
'__proto__': 2,
423+
'constructor': 2,
424+
'prototype': 2
425+
}
426+
const C = Value.Diff(A, B)
427+
Assert.IsEqual(C, [{ type: 'update', path: '/value', value: 2 }])
428+
})
429+
Test('Should not generate edits for nested unsafe properties on DELETE', () => {
430+
const A = {
431+
outer: {
432+
value: 1,
433+
'__proto__': 1,
434+
'constructor': 1,
435+
'prototype': 1
436+
}
437+
}
438+
const B = { outer: {} }
439+
const C = Value.Diff(A, B)
440+
Assert.IsEqual(C, [{ type: 'delete', path: '/outer/value' }])
441+
})
442+
Test('Should not generate edits for nested unsafe properties on INSERT', () => {
443+
const A = { outer: {} }
444+
const B = {
445+
outer: {
446+
value: 1,
447+
'__proto__': 1,
448+
'constructor': 1,
449+
'prototype': 1
450+
}
451+
}
452+
const C = Value.Diff(A, B)
453+
Assert.IsEqual(C, [{ type: 'insert', path: '/outer/value', value: 1 }])
454+
})

0 commit comments

Comments
 (0)