Skip to content

Commit eaed090

Browse files
committed
fix(md): escape template literal metacharacters in bash fence blocks
1 parent 707b43a commit eaed090

2 files changed

Lines changed: 45 additions & 1 deletion

File tree

src/md.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,13 @@ export function transformMarkdown(buf: Buffer | string): string {
8888
prevEmpty = true
8989
fenceChar = ''
9090
} else {
91-
const s = stripRe ? line.replace(stripRe, '') : line
91+
let s = stripRe ? line.replace(stripRe, '') : line
92+
if (closeOut === '`') {
93+
s = s
94+
.replace(/\\/g, '\\\\')
95+
.replace(/`/g, '\\`')
96+
.replace(/\${/g, '\\${')
97+
}
9298
out.push(linePrefix + s)
9399
prevEmpty = false
94100
}

test/md.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,42 @@ echo "4"
122122
const expected = '// a\n// b\n// c\n// d\n// e\n// f'
123123
assert.equal(transformMarkdown(input), expected)
124124
})
125+
126+
describe('security: bash fence injection prevention', () => {
127+
test('escapes ${...} interpolation in bash fences to prevent JS injection', () => {
128+
const result = transformMarkdown(
129+
'```bash\necho ${require("child_process").execSync("id")}\n```'
130+
)
131+
assert.ok(
132+
result.includes('\\${require'),
133+
'interpolation not escaped in bash fence output'
134+
)
135+
})
136+
137+
test('escapes backticks in bash fences to prevent template literal breakout', () => {
138+
const result = transformMarkdown('```bash\necho `uname -s`\n```')
139+
assert.ok(
140+
result.includes('\\`uname'),
141+
'backtick not escaped in bash fence output'
142+
)
143+
})
144+
145+
test('does not escape ${...} in js fences (intended interpolation)', () => {
146+
const result = transformMarkdown(
147+
'```js\nconsole.log(`${process.env.HOME}`)\n```'
148+
)
149+
assert.ok(
150+
result.includes('${process.env.HOME}'),
151+
'JS interpolation was incorrectly escaped in js fence'
152+
)
153+
})
154+
155+
test('bash fence $VAR and $(cmd) pass through unmodified', () => {
156+
const result = transformMarkdown(
157+
'```bash\necho $HOME\necho $(whoami)\n```'
158+
)
159+
assert.ok(result.includes('echo $HOME'), '$VAR incorrectly modified')
160+
assert.ok(result.includes('echo $(whoami)'), '$() incorrectly modified')
161+
})
162+
})
125163
})

0 commit comments

Comments
 (0)