Skip to content

Commit a3a8092

Browse files
committed
fix: escape schema values in generated code
1 parent e02b5bb commit a3a8092

3 files changed

Lines changed: 183 additions & 16 deletions

File tree

index.js

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ function buildExtraObjectPropertiesSerializer (context, location, addComma, objV
371371
const propertyLocation = patternPropertiesLocation.getPropertyLocation(propertyKey)
372372

373373
code += `
374-
if (/${propertyKey.replace(/\\*\//g, '\\/')}/.test(key)) {
374+
if (new RegExp(${JSON.stringify(propertyKey)}).test(key)) {
375375
${addComma}
376376
json += asString(key) + JSON_STR_COLONS
377377
${buildValue(context, propertyLocation, 'value')}
@@ -620,10 +620,7 @@ function buildObject (context, location, input) {
620620
const functionName = generateFuncName(context)
621621
context.functionsNamesBySchema.set(schema, functionName)
622622

623-
const schemaRef = getSafeSchemaRef(context, location)
624-
625623
const functionCode = `
626-
// ${schemaRef}
627624
function ${functionName} (input) {
628625
const obj = ${toJSON('input')}
629626
if (obj === null) return ${nullable ? 'JSON_STR_NULL' : 'JSON_STR_EMPTY_OBJECT'}
@@ -680,17 +677,17 @@ function buildArray (context, location, input) {
680677
context.functionsNamesBySchema.set(schema, functionName)
681678

682679
const schemaRef = getSafeSchemaRef(context, location)
680+
const schemaRefError = JSON.stringify(`The value of '${schemaRef}' does not match schema definition.`)
683681

684682
let functionCode = `
685683
function ${functionName} (obj) {
686-
// ${schemaRef}
687684
let json = ''
688685
`
689686

690687
functionCode += `
691688
if (obj === null) return ${nullable ? 'JSON_STR_NULL' : 'JSON_STR_EMPTY_ARRAY'}
692689
if (!Array.isArray(obj)) {
693-
throw new TypeError(\`The value of '${schemaRef}' does not match schema definition.\`)
690+
throw new TypeError(${schemaRefError})
694691
}
695692
const arrayLength = obj.length
696693
`
@@ -768,14 +765,15 @@ function buildArray (context, location, input) {
768765
}
769766

770767
context.buildingSet.add(schema)
771-
const safeSchemaRef = getSafeSchemaRef(context, location)
768+
const schemaRef = getSafeSchemaRef(context, location)
769+
const schemaRefError = JSON.stringify(`The value of '${schemaRef}' does not match schema definition.`)
772770
const objVar = `obj_${context.uid++}`
773771
let inlinedCode = `
774772
const ${objVar} = ${input}
775773
if (${objVar} === null) {
776774
json += ${nullable ? 'JSON_STR_NULL' : 'JSON_STR_EMPTY_ARRAY'}
777775
} else if (!Array.isArray(${objVar})) {
778-
throw new TypeError(\`The value of '${safeSchemaRef}' does not match schema definition.\`)
776+
throw new TypeError(${schemaRefError})
779777
} else {
780778
const arrayLength_${objVar} = ${objVar}.length
781779
`
@@ -976,8 +974,9 @@ function buildMultiTypeSerializer (context, location, input) {
976974
}
977975
}
978976
})
977+
const schemaRef = getSafeSchemaRef(context, location)
979978
code += `
980-
else throw new TypeError(\`The value of '${getSafeSchemaRef(context, location)}' does not match schema definition.\`)
979+
else throw new TypeError(${JSON.stringify(`The value of '${schemaRef}' does not match schema definition.`)})
981980
`
982981

983982
return code
@@ -1218,14 +1217,15 @@ function buildOneOf (context, location, input) {
12181217
context.validatorSchemaRefs.add(schemaRef)
12191218

12201219
code += `
1221-
${index === 0 ? 'if' : 'else if'}(validator.validate("${schemaRef}", ${input})) {
1220+
${index === 0 ? 'if' : 'else if'}(validator.validate(${JSON.stringify(schemaRef)}, ${input})) {
12221221
${nestedResult}
12231222
}
12241223
`
12251224
}
12261225

1226+
const schemaRef = getSafeSchemaRef(context, location)
12271227
code += `
1228-
else throw new TypeError(\`The value of '${getSafeSchemaRef(context, location)}' does not match schema definition.\`)
1228+
else throw new TypeError(${JSON.stringify(`The value of '${schemaRef}' does not match schema definition.`)})
12291229
`
12301230

12311231
return code
@@ -1268,7 +1268,7 @@ function buildIfThenElse (context, location, input) {
12681268

12691269
if (!elseSchema) {
12701270
return `
1271-
if (validator.validate("${ifSchemaRef}", ${input})) {
1271+
if (validator.validate(${JSON.stringify(ifSchemaRef)}, ${input})) {
12721272
${buildValue(context, thenMergedLocation, input)}
12731273
} else {
12741274
${buildValue(context, rootLocation, input)}
@@ -1292,7 +1292,7 @@ function buildIfThenElse (context, location, input) {
12921292
}
12931293

12941294
return `
1295-
if (validator.validate("${ifSchemaRef}", ${input})) {
1295+
if (validator.validate(${JSON.stringify(ifSchemaRef)}, ${input})) {
12961296
${buildValue(context, thenMergedLocation, input)}
12971297
} else {
12981298
${buildValue(context, elseMergedLocation, input)}

test/code-generation-fallbacks.test.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,18 +110,20 @@ test('inline array generation without schema IDs', t => {
110110
})
111111

112112
test('code generation reference fallbacks', t => {
113-
t.plan(3)
113+
t.plan(4)
114114

115115
const buildWithoutPointer = loadBuildWithLocation(LocationWithoutJsonPointer)
116116
const stringifyObject = buildWithoutPointer({ type: 'object' })
117117
const stringifyArray = buildWithoutPointer({ type: 'array' })
118118

119119
const buildWithoutRef = loadBuildWithLocation(LocationWithoutSchemaRef)
120-
const stringifyWithoutRef = buildWithoutRef({ type: 'object' })
120+
const stringifyObjectWithoutRef = buildWithoutRef({ type: 'object' })
121+
const stringifyArrayWithoutRef = buildWithoutRef({ type: 'array' })
121122

122123
t.assert.equal(stringifyObject({}), '{}')
123124
t.assert.equal(stringifyArray([]), '[]')
124-
t.assert.equal(stringifyWithoutRef({}), '{}')
125+
t.assert.equal(stringifyObjectWithoutRef({}), '{}')
126+
t.assert.equal(stringifyArrayWithoutRef([]), '[]')
125127
})
126128

127129
test('required-property fallback tolerates unexpected property ordering', t => {
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
'use strict'
2+
3+
const { test } = require('node:test')
4+
const build = require('..')
5+
6+
const marker = '__fastJsonStringifyCodeGenerationMarker'
7+
8+
test('schema references with line breaks do not alter generated code', (t) => {
9+
t.after(() => {
10+
delete globalThis[marker]
11+
})
12+
13+
const objectPropertyName = `nested\n;globalThis.${marker} = true\n//`
14+
const arrayPropertyName = `list\n;globalThis.${marker} = true\n//`
15+
const input = {
16+
[objectPropertyName]: {
17+
value: 'safe'
18+
},
19+
[arrayPropertyName]: ['safe']
20+
}
21+
22+
const stringify = build({
23+
type: 'object',
24+
properties: {
25+
[objectPropertyName]: {
26+
type: 'object',
27+
properties: {
28+
value: { type: 'string' }
29+
}
30+
},
31+
[arrayPropertyName]: {
32+
type: 'array',
33+
items: { type: 'string' }
34+
}
35+
}
36+
})
37+
38+
t.assert.equal(globalThis[marker], undefined)
39+
const output = stringify(input)
40+
t.assert.equal(globalThis[marker], undefined)
41+
t.assert.equal(output, JSON.stringify(input))
42+
})
43+
44+
function testSchemaRefError (name, propertySchema, invalidValue) {
45+
test(`schema references are escaped in generated ${name} errors`, (t) => {
46+
t.after(() => {
47+
delete globalThis[marker]
48+
})
49+
50+
const propertyName = `${name}\`,globalThis.${marker}=true,\``
51+
const stringify = build({
52+
type: 'object',
53+
properties: {
54+
[propertyName]: propertySchema
55+
}
56+
})
57+
58+
t.assert.throws(
59+
() => stringify({ [propertyName]: invalidValue }),
60+
new TypeError(`The value of '#/properties/${propertyName}' does not match schema definition.`)
61+
)
62+
t.assert.equal(globalThis[marker], undefined)
63+
})
64+
}
65+
66+
testSchemaRefError('array', {
67+
type: 'array',
68+
items: { type: 'string' }
69+
}, 'not an array')
70+
71+
testSchemaRefError('multi-type', {
72+
type: ['string', 'number']
73+
}, {})
74+
75+
testSchemaRefError('anyOf', {
76+
anyOf: [
77+
{ type: 'string' },
78+
{ type: 'number' }
79+
]
80+
}, false)
81+
82+
test('pattern property expressions are preserved in generated code', (t) => {
83+
const newlinePattern = '^line\nbreak$'
84+
const slashPattern = '^backslash\\\\/$'
85+
const stringify = build({
86+
type: 'object',
87+
patternProperties: {
88+
[newlinePattern]: { type: 'string' },
89+
[slashPattern]: { type: 'string' }
90+
}
91+
})
92+
93+
const input = {
94+
'line\nbreak': 'newline',
95+
'backslash\\/': 'slash',
96+
'backslash/': 'does not match'
97+
}
98+
99+
t.assert.equal(stringify(input), '{"line\\nbreak":"newline","backslash\\\\/":"slash"}')
100+
})
101+
102+
test('quoted external schema ids are escaped in anyOf validator calls', (t) => {
103+
const stringSchemaId = 'external"string'
104+
const numberSchemaId = 'external"number'
105+
const stringify = build({
106+
anyOf: [
107+
{ $ref: `${stringSchemaId}#` },
108+
{ $ref: `${numberSchemaId}#` }
109+
]
110+
}, {
111+
schema: {
112+
stringSchema: {
113+
$id: stringSchemaId,
114+
type: 'string'
115+
},
116+
numberSchema: {
117+
$id: numberSchemaId,
118+
type: 'number'
119+
}
120+
}
121+
})
122+
123+
t.assert.equal(stringify('safe'), '"safe"')
124+
t.assert.equal(stringify(42), '42')
125+
})
126+
127+
test('quoted external schema ids are escaped in if validator calls', (t) => {
128+
const schemaId = 'condition"schema'
129+
const thenSchema = {
130+
properties: {
131+
value: { type: 'string' }
132+
}
133+
}
134+
const options = {
135+
schema: {
136+
condition: {
137+
$id: schemaId,
138+
type: 'object',
139+
properties: {
140+
kind: { const: 'string' }
141+
},
142+
required: ['kind']
143+
}
144+
}
145+
}
146+
const stringify = build({
147+
type: 'object',
148+
if: { $ref: `${schemaId}#` },
149+
then: thenSchema,
150+
else: {
151+
properties: {
152+
value: { type: 'number' }
153+
}
154+
}
155+
}, options)
156+
const stringifyWithoutElse = build({
157+
type: 'object',
158+
if: { $ref: `${schemaId}#` },
159+
then: thenSchema
160+
}, options)
161+
162+
t.assert.equal(stringify({ kind: 'string', value: 'safe' }), '{"value":"safe"}')
163+
t.assert.equal(stringify({ kind: 'number', value: 42 }), '{"value":42}')
164+
t.assert.equal(stringifyWithoutElse({ kind: 'string', value: 'safe' }), '{"value":"safe"}')
165+
})

0 commit comments

Comments
 (0)