Skip to content

Commit 78c5e8b

Browse files
committed
fix(execution): harden generated JavaScript literals
1 parent 9661638 commit 78c5e8b

2 files changed

Lines changed: 44 additions & 4 deletions

File tree

apps/sim/lib/execution/code-placeholders/compiler.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,36 @@ describe('code placeholder compiler', () => {
205205
})
206206
})
207207

208+
it('round-trips source-sensitive string and tagged-template characters', async () => {
209+
const sourceText = `</script>${String.fromCharCode(0x2028, 0x2029)}`
210+
const secret = `secret"\\\n${sourceText}`
211+
const literalText = `before ${sourceText} {{KEY}} after`
212+
const compiled = await compileCodePlaceholders({
213+
code: [
214+
`const quoted = ${JSON.stringify(literalText)}`,
215+
'const tag = (strings) => ({ cooked: strings[0], raw: strings.raw[0] })',
216+
`const tagged = tag\`${literalText}\``,
217+
'return { quoted, tagged }',
218+
].join('\n'),
219+
language: CodeLanguage.JavaScript,
220+
environmentVariables: { KEY: secret },
221+
})
222+
223+
const expected = `before ${sourceText} ${secret} after`
224+
expect(compiled.code).toContain('\\u003c/script\\u003e')
225+
expect(compiled.code).toContain('\\u2028\\u2029')
226+
expect(compiled.code).not.toContain('</script>')
227+
expect(compiled.code).not.toContain(String.fromCharCode(0x2028))
228+
expect(compiled.code).not.toContain(String.fromCharCode(0x2029))
229+
expect(compiled.code).not.toContain(secret)
230+
await expect(
231+
executeJavaScript(compiled.code, compiled.bindings, compiled.runtimeBindings)
232+
).resolves.toEqual({
233+
quoted: expected,
234+
tagged: { cooked: expected, raw: expected },
235+
})
236+
})
237+
208238
it('leaves JavaScript comments and missing placeholders untouched', async () => {
209239
const code = [
210240
'// {{COMMENT}}',

apps/sim/lib/execution/code-placeholders/javascript.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,16 @@ function restoreUnresolvedSentinels(value: string, items: SentinelOccurrence[]):
285285
return value.replace(/\$[0-9A-Za-z]*\$/g, (sentinel) => rawBySentinel.get(sentinel) ?? sentinel)
286286
}
287287

288+
const UNSAFE_JAVASCRIPT_SOURCE_CHARACTERS = /[<>\u2028\u2029]/g
289+
290+
/** Serializes a string literal without embedding HTML terminators or JavaScript line separators. */
291+
function serializeJavaScriptStringLiteral(value: string): string {
292+
return JSON.stringify(value).replace(
293+
UNSAFE_JAVASCRIPT_SOURCE_CHARACTERS,
294+
(character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`
295+
)
296+
}
297+
288298
function buildStringExpression(
289299
value: string,
290300
items: SentinelOccurrence[],
@@ -306,13 +316,13 @@ function buildStringExpression(
306316
const resolved = resolve(item.occurrence)
307317
if (!resolved) continue
308318
matched = true
309-
parts.push(JSON.stringify(restore(value.slice(cursor, match.index))))
319+
parts.push(serializeJavaScriptStringLiteral(restore(value.slice(cursor, match.index))))
310320
parts.push(resolved.bindingName)
311321
cursor = match.index + match[0].length
312322
consumed.add(item.occurrence)
313323
}
314324
if (!matched) return undefined
315-
parts.push(JSON.stringify(restore(value.slice(cursor))))
325+
parts.push(serializeJavaScriptStringLiteral(restore(value.slice(cursor))))
316326
return `(${parts.join(' + ')})`
317327
}
318328

@@ -593,7 +603,7 @@ export async function compileJavaScriptPlaceholders(
593603
}
594604
const segmentExpression = (value: string, items: SentinelOccurrence[]): string =>
595605
buildStringExpression(value, items, context.resolve, consumed) ??
596-
JSON.stringify(restoreUnresolvedSentinels(value, items))
606+
serializeJavaScriptStringLiteral(restoreUnresolvedSentinels(value, items))
597607
const cookedSegments = tokens.map((token, index) =>
598608
hasInvalidTemplateEscape(token)
599609
? 'undefined'
@@ -684,7 +694,7 @@ export async function compileJavaScriptPlaceholders(
684694
edits.push({
685695
start: node.getStart(sourceFile),
686696
end: node.getEnd(),
687-
text: `new ${intrinsics.name}.RegExp(${expression}, ${JSON.stringify(parsed.flags)})`,
697+
text: `new ${intrinsics.name}.RegExp(${expression}, ${serializeJavaScriptStringLiteral(parsed.flags)})`,
688698
})
689699
return
690700
}

0 commit comments

Comments
 (0)