Skip to content

Commit 9dfab5f

Browse files
committed
fix(code-placeholders): harden shell placeholder interpolation in arithmetic contexts
1 parent 14e6c0a commit 9dfab5f

3 files changed

Lines changed: 397 additions & 157 deletions

File tree

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

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,6 +1053,43 @@ describe('code placeholder compiler', () => {
10531053
'total=$(( $(( 1 + 1 )) + {{KEY}} ))',
10541054
'cat <<PAYLOAD\n$(( {{KEY}} * 2 ))\nPAYLOAD',
10551055
'cat <<PAYLOAD\n$[ {{KEY}} * 2 ]\nPAYLOAD',
1056+
'echo $(( $(cat <<EOF\n{{KEY}}\nEOF\n) + 1 ))',
1057+
"echo $(( $(cat <<'EOF'\n{{KEY}}\nEOF\n) + 1 ))",
1058+
"echo $(( $(cat <<-'EOF'\n\t{{KEY}}\n\tEOF\n) + 1 ))",
1059+
'[[ {{KEY}} -eq 0 ]] && echo zero',
1060+
'[[ "{{KEY}}" -eq 0 ]] && echo zero',
1061+
'if [[ 0 -lt {{KEY}} ]]; then echo positive; fi',
1062+
'[[ -n x && ( "{{KEY}}" -ge 1 ) ]]',
1063+
'[[ $(printf "%s" "{{KEY}}") -ne 0 ]]',
1064+
'[[ $(printf "%s" "{{KEY}}"; echo 1) -le 0 ]]',
1065+
'[[ $(cat <<EOF\n{{KEY}}\nEOF\n) -gt 0 ]]',
1066+
'let "x={{KEY}}"',
1067+
'let x={{KEY}}+1',
1068+
'declare -i x="{{KEY}}"',
1069+
'typeset -i x={{KEY}}',
1070+
'f() { local -i x="{{KEY}}"; }',
1071+
'declare -ai values=("{{KEY}}")',
1072+
'declare -x -i x="{{KEY}}"',
1073+
'declare -i x; x="{{KEY}}"',
1074+
'f() { x+={{KEY}}; }; typeset -i x',
1075+
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1076+
'a=(1 2); echo "${a[{{KEY}}]}"',
1077+
'a[{{KEY}}]=1',
1078+
'a=([{{KEY}}]=1)',
1079+
'test -v "a[{{KEY}}]"',
1080+
'read "a[{{KEY}}]" <<< x',
1081+
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1082+
's=abc; echo "${s:{{KEY}}}"',
1083+
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1084+
's=abc; echo "${s:0:{{KEY}}}"',
1085+
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1086+
's=abc; echo "${s: -1:{{KEY}}}"',
1087+
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1088+
'set -- a b; echo "${@:{{KEY}}}"',
1089+
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1090+
'cat <<PAYLOAD\n${a[{{KEY}}]}\nPAYLOAD',
1091+
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1092+
'cat <<PAYLOAD\n${s:{{KEY}}}\nPAYLOAD',
10561093
])('rejects shell placeholders whose values enter arithmetic: %s', async (code) => {
10571094
await expect(
10581095
compileCodePlaceholders({
@@ -1087,6 +1124,32 @@ describe('code placeholder compiler', () => {
10871124
)
10881125
})
10891126

1127+
it('compiles shell placeholders beside arithmetic that never evaluates them', async () => {
1128+
const value = 'values[$(printf injected >&2)]'
1129+
const compiled = await compileCodePlaceholders({
1130+
code: [
1131+
'[ "{{KEY}}" -eq 0 ] 2>/dev/null || printf "%s\\n" not-zero',
1132+
'[[ "{{KEY}}" == values* && 1 -eq 1 ]] && printf "%s\\n" matched',
1133+
'let total=1+1; printf "%s\\n" "{{KEY}}"',
1134+
'f() { local copy="{{KEY}}"; printf "%s\\n" "$copy"; }; f',
1135+
'declare copy="{{KEY}}"; printf "%s\\n" "$copy"',
1136+
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1137+
'printf "%s\\n" "${missing:-{{KEY}}}"',
1138+
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell parameter expansion, not a JS template
1139+
'a=(x y); printf "%s\\n" "${a[1]}{{KEY}}"',
1140+
'cat <<PAYLOAD',
1141+
'{{KEY}} $(( 1 + 1 ))',
1142+
'PAYLOAD',
1143+
].join('\n'),
1144+
language: CodeLanguage.Shell,
1145+
environmentVariables: { KEY: value },
1146+
})
1147+
1148+
expect(executeShell(`{\n${compiled.code}\n} 2>&1`, compiled.bindings)).toBe(
1149+
`not-zero\nmatched\n${value}\n${value}\n${value}\n${value}\ny${value}\n${value} 2\n`
1150+
)
1151+
})
1152+
10901153
it('discovers shell arithmetic placeholders without compiling missing values', async () => {
10911154
const code = 'total=$(( {{MISSING}} + {{KEY}} ))'
10921155
await expect(analyzeCodePlaceholders(code, CodeLanguage.Shell)).resolves.toEqual([

‎apps/sim/lib/execution/code-placeholders/shared.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -327,12 +327,12 @@ export function createOffsetRangeLookup(
327327
* The placeholders lying wholly inside `[start, end]`. Occurrences are ordered and never
328328
* overlap, so the matches are one contiguous run found by bisection.
329329
*/
330-
export function occurrencesWithin(
331-
occurrences: readonly CodePlaceholderOccurrence[],
330+
export function occurrencesWithin<T extends Pick<CodePlaceholderOccurrence, 'start' | 'end'>>(
331+
occurrences: readonly T[],
332332
start: number,
333333
end: number
334-
): CodePlaceholderOccurrence[] {
335-
const matches: CodePlaceholderOccurrence[] = []
334+
): T[] {
335+
const matches: T[] = []
336336
for (
337337
let index = partitionPoint(occurrences, (occurrence) => occurrence.start < start);
338338
index < occurrences.length && occurrences[index].end <= end;

0 commit comments

Comments
 (0)