Skip to content

Commit 4600846

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(oracle-fusion): close validation edge cases
1 parent 584f770 commit 4600846

6 files changed

Lines changed: 62 additions & 14 deletions

File tree

apps/sim/lib/internal/oracle-fusion/identifiers.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,12 @@ describe('normalizeOracleFusionDecimalIdentifier', () => {
7070
normalizeOracleFusionDecimalIdentifier('1', { maxDigits: 129, maxSourceLength: 128 })
7171
).toThrow('limits are invalid')
7272
})
73+
74+
it('checks the digit limit after removing an exact fractional suffix', () => {
75+
expect(
76+
normalizeOracleFusionDecimalIdentifier('123456000.000', {
77+
maxDigits: 8,
78+
})
79+
).toBeUndefined()
80+
})
7381
})

apps/sim/lib/internal/oracle-fusion/identifiers.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,5 +103,7 @@ export function normalizeOracleFusionDecimalIdentifier(
103103
if (fractionalDigits > significantCoefficient.length) return undefined
104104
const suffix = significantCoefficient.slice(significantCoefficient.length - fractionalDigits)
105105
if (!/^0*$/.test(suffix)) return undefined
106-
return significantCoefficient.slice(0, significantCoefficient.length - fractionalDigits) || '0'
106+
const normalized =
107+
significantCoefficient.slice(0, significantCoefficient.length - fractionalDigits) || '0'
108+
return normalized.length <= options.maxDigits ? normalized : undefined
107109
}

apps/sim/lib/internal/oracle-fusion/protocol.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,16 @@ describe('Oracle self links', () => {
170170
)
171171
})
172172

173+
it('rejects a self-link href containing malformed Unicode before URL parsing', () => {
174+
expect(() =>
175+
extractOracleFusionOpaqueKey(
176+
resource(`${ORIGIN}${COLLECTION}/bad\ud800key`),
177+
ORIGIN,
178+
COLLECTION_ADDRESS
179+
)
180+
).toThrow('Oracle self link is malformed')
181+
})
182+
173183
it.each([
174184
[`${ORIGIN}/other/abc`, 'collection path'],
175185
[`${ORIGIN}${COLLECTION}/a/b`, 'one opaque key'],

apps/sim/lib/internal/oracle-fusion/protocol.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@ import {
77
const OPAQUE_KEY_MAX_LENGTH = 2048
88
const UNSAFE_OPAQUE_KEY = /[\\/?#\u0000-\u001f\u007f]/
99

10+
function hasWellFormedUtf16(value: string): boolean {
11+
for (let index = 0; index < value.length; index++) {
12+
const codeUnit = value.charCodeAt(index)
13+
if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
14+
const next = value.charCodeAt(index + 1)
15+
if (!(next >= 0xdc00 && next <= 0xdfff)) return false
16+
index++
17+
} else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
18+
return false
19+
}
20+
}
21+
return true
22+
}
23+
1024
export interface OracleFusionCollection<T> {
1125
items: T[]
1226
count: number
@@ -111,7 +125,9 @@ function getOnlySelfLink(value: unknown): URL {
111125
throw new Error('Oracle response must include exactly one self link')
112126
}
113127
const href = (selfLinks[0] as Record<string, unknown>).href
114-
if (typeof href !== 'string') throw new Error('Oracle self link is malformed')
128+
if (typeof href !== 'string' || !hasWellFormedUtf16(href)) {
129+
throw new Error('Oracle self link is malformed')
130+
}
115131
try {
116132
return new URL(href)
117133
} catch {
@@ -157,17 +173,8 @@ function validateOpaqueKey(key: string): string {
157173
) {
158174
throw new Error('Oracle resource key is not a safe opaque path segment')
159175
}
160-
for (let index = 0; index < key.length; index++) {
161-
const codeUnit = key.charCodeAt(index)
162-
if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
163-
const next = key.charCodeAt(index + 1)
164-
if (!(next >= 0xdc00 && next <= 0xdfff)) {
165-
throw new Error('Oracle resource key contains malformed Unicode')
166-
}
167-
index++
168-
} else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
169-
throw new Error('Oracle resource key contains malformed Unicode')
170-
}
176+
if (!hasWellFormedUtf16(key)) {
177+
throw new Error('Oracle resource key contains malformed Unicode')
171178
}
172179
return key
173180
}

apps/sim/lib/internal/oracle-fusion/request-body.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,20 @@ describe('serializeOracleFusionJsonBody', () => {
6464
expect(() => serializeOracleFusionJsonBody(customArray)).toThrow('plain JSON data')
6565
})
6666

67+
it('rejects inherited custom serialization before JSON.stringify can invoke it', () => {
68+
const previous = Object.getOwnPropertyDescriptor(Array.prototype, 'toJSON')
69+
Object.defineProperty(Array.prototype, 'toJSON', {
70+
configurable: true,
71+
value: () => ({ replaced: true }),
72+
})
73+
try {
74+
expect(() => serializeOracleFusionJsonBody([1])).toThrow('plain JSON data')
75+
} finally {
76+
if (previous) Object.defineProperty(Array.prototype, 'toJSON', previous)
77+
else Reflect.deleteProperty(Array.prototype, 'toJSON')
78+
}
79+
})
80+
6781
it('rejects cycles, excessive nesting, and excessive complexity', () => {
6882
const cycle: unknown[] = []
6983
cycle.push(cycle)

apps/sim/lib/internal/oracle-fusion/request-body.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,11 +134,18 @@ function isRecordLike(value: unknown): value is Record<string, unknown> {
134134
}
135135

136136
function assertContainerIsPlain(value: object): void {
137+
for (
138+
let candidate: object | null = value;
139+
candidate;
140+
candidate = Object.getPrototypeOf(candidate)
141+
) {
142+
if (Object.hasOwn(candidate, 'toJSON')) throwNonPlainJsonError()
143+
}
137144
for (const key of Reflect.ownKeys(value)) {
138145
if (typeof key === 'symbol') throwNonPlainJsonError()
139146
if (key === 'length' && Array.isArray(value)) continue
140147
const descriptor = Object.getOwnPropertyDescriptor(value, key)
141-
if (descriptor?.get || descriptor?.set || key === 'toJSON') throwNonPlainJsonError()
148+
if (descriptor?.get || descriptor?.set) throwNonPlainJsonError()
142149
if (Array.isArray(value)) {
143150
const index = Number(key)
144151
if (

0 commit comments

Comments
 (0)