mirror of https://github.com/vuejs/vitepress
fix: serialize config functions as code, not strings revived via new Function (close #3685)
Functions in the site config (e.g. a custom miniSearch tokenize) were embedded in the client's JSON payload as source strings and rebuilt at runtime with new Function, which strict CSP blocks unless unsafe-eval is allowed. Emit them as plain function expressions in the generated script instead, with only an index marker left in the JSON, so the deserializer just looks them up. This also stops data strings that merely start with "_vp-fn_" from being evaluated as code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>pull/5408/head
parent
feadd9fcc1
commit
d3b2957db0
@ -1 +1,3 @@
|
||||
# Local search included
|
||||
|
||||
The custom tokenizer keeps #hash-probe and hyphen-linked-words whole.
|
||||
|
||||
@ -0,0 +1,98 @@
|
||||
import {
|
||||
deserializeFunctions,
|
||||
serializeFunctions
|
||||
} from 'node/utils/fnSerialize'
|
||||
|
||||
// runs the exact code shape that plugin.ts / build.ts emit into the site-data
|
||||
// module and the metadata script — the revived value must come back without
|
||||
// the deserializer ever compiling a string (new Function is used here only to
|
||||
// stand in for the browser executing the emitted file)
|
||||
function emitAndRevive(data: any): any {
|
||||
const fns: string[] = []
|
||||
const serialized = serializeFunctions(data, fns)
|
||||
const script = `${deserializeFunctions};return deserializeFunctions(JSON.parse(${JSON.stringify(
|
||||
JSON.stringify(serialized)
|
||||
)}),[${fns.join(',')}])`
|
||||
return new Function(script)()
|
||||
}
|
||||
|
||||
describe('node/utils/fnSerialize', () => {
|
||||
test('emitted deserializer does not rely on unsafe-eval', () => {
|
||||
expect(deserializeFunctions).not.toContain('new Function')
|
||||
expect(deserializeFunctions).not.toContain('eval')
|
||||
})
|
||||
|
||||
test('serializes functions as indexed markers', () => {
|
||||
const fns: string[] = []
|
||||
const serialized = serializeFunctions(
|
||||
{ a: (x: number) => x, b: { c: (x: number) => x * 2 } },
|
||||
fns
|
||||
)
|
||||
expect(serialized).toEqual({ a: '_vp-fn_0', b: { c: '_vp-fn_1' } })
|
||||
expect(fns).toHaveLength(2)
|
||||
})
|
||||
|
||||
test('revives functions nested in objects and arrays', () => {
|
||||
const data = {
|
||||
search: {
|
||||
options: {
|
||||
miniSearch: {
|
||||
options: {
|
||||
tokenize: (text: string) => text.split(/\s+/)
|
||||
},
|
||||
searchOptions: {
|
||||
boostDocument: (id: string) => (id === 'index.md' ? 2 : 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
list: [(n: number) => n + 1, 'plain', 42]
|
||||
}
|
||||
|
||||
const revived = emitAndRevive(data)
|
||||
|
||||
expect(revived.search.options.miniSearch.options.tokenize('a b')).toEqual([
|
||||
'a',
|
||||
'b'
|
||||
])
|
||||
expect(
|
||||
revived.search.options.miniSearch.searchOptions.boostDocument('index.md')
|
||||
).toBe(2)
|
||||
expect(revived.list[0](1)).toBe(2)
|
||||
expect(revived.list[1]).toBe('plain')
|
||||
expect(revived.list[2]).toBe(42)
|
||||
})
|
||||
|
||||
test('revives method shorthand and async functions', () => {
|
||||
const data = {
|
||||
tokenize(text: string) {
|
||||
return text.toUpperCase()
|
||||
},
|
||||
async extractField(doc: { id: string }) {
|
||||
return doc.id
|
||||
}
|
||||
}
|
||||
|
||||
const revived = emitAndRevive(data)
|
||||
|
||||
expect(revived.tokenize('abc')).toBe('ABC')
|
||||
return expect(revived.extractField({ id: 'x' })).resolves.toBe('x')
|
||||
})
|
||||
|
||||
test('drops underscore-prefixed keys', () => {
|
||||
const revived = emitAndRevive({ _render: () => '', keep: 1 })
|
||||
expect(revived).toEqual({ keep: 1 })
|
||||
})
|
||||
|
||||
test('leaves data strings resembling markers untouched', () => {
|
||||
const data = {
|
||||
fn: (x: number) => x,
|
||||
note: '_vp-fn_alert(1)'
|
||||
}
|
||||
|
||||
const revived = emitAndRevive(data)
|
||||
|
||||
expect(revived.fn(1)).toBe(1)
|
||||
expect(revived.note).toBe('_vp-fn_alert(1)')
|
||||
})
|
||||
})
|
||||
Loading…
Reference in new issue