Skip to content

Commit 67ee28b

Browse files
authored
fix: handle escaped dots in yaml lookups (#3450)
1 parent 8aec084 commit 67ee28b

2 files changed

Lines changed: 78 additions & 2 deletions

File tree

‎test/integration/test-builder.js‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -403,10 +403,27 @@ function indent (str, spaces) {
403403
return str.replace(/\s+$/, '').split('\n').map(l => `${tabs}${l}`).join('\n') + '\n'
404404
}
405405

406+
function splitLookupPath (path) {
407+
const steps = []
408+
let cur = ''
409+
for (let i = 0; i < path.length; i++) {
410+
if (path[i] === '\\' && i + 1 < path.length) {
411+
cur += path[++i]
412+
} else if (path[i] === '.') {
413+
steps.push(cur)
414+
cur = ''
415+
} else {
416+
cur += path[i]
417+
}
418+
}
419+
steps.push(cur)
420+
return steps
421+
}
422+
406423
function buildLookup (path) {
407424
if (path === '$body') return '(typeof response.body === "string" ? response.body : JSON.stringify(response.body))'
408425

409-
const outPath = path.split('.').map(step => {
426+
const outPath = splitLookupPath(String(path)).map(step => {
410427
if (parseInt(step, 10).toString() === step) {
411428
return `?.[${step}]`
412429
} else if (step.match(/^\$[a-zA-Z0-9_]+$/)) {
@@ -416,7 +433,7 @@ function buildLookup (path) {
416433
} else if (step === '') {
417434
return ''
418435
} else {
419-
return `?.['${step}']`
436+
return `?.[${JSON.stringify(step)}]`
420437
}
421438
}).join('')
422439
return `response.body${outPath}`
@@ -470,3 +487,4 @@ function isPlainObject (obj) {
470487
}
471488

472489
module.exports = build
490+
module.exports.buildLookup = buildLookup

‎test/unit/test-builder.test.ts‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { test } from 'tap'
7+
import { execFileSync } from 'node:child_process'
8+
9+
const { buildLookup } = require('../integration/test-builder.js') as {
10+
buildLookup: (path: string) => string
11+
}
12+
13+
function evalLookup (path: string, body: unknown): unknown {
14+
const expr = buildLookup(path)
15+
const script = `const response = { body: ${JSON.stringify(body)} }; process.stdout.write(JSON.stringify(${expr}))`
16+
return JSON.parse(execFileSync(process.execPath, ['-e', script], { encoding: 'utf8' }))
17+
}
18+
19+
function assertValidJs (t: { doesNotThrow: (fn: () => void) => void }, path: string): void {
20+
const expr = buildLookup(path)
21+
t.doesNotThrow(() => execFileSync(process.execPath, ['-e', `const response = {body:{}}; ${expr}`]))
22+
}
23+
24+
test('escaped dots stay one key', t => {
25+
t.equal(buildLookup('logs\\.otel.enabled'), 'response.body?.["logs.otel"]?.["enabled"]')
26+
t.equal(evalLookup('logs\\.otel.enabled', { 'logs.otel': { enabled: true } }), true)
27+
t.end()
28+
})
29+
30+
test('plain dotted path still splits', t => {
31+
t.equal(evalLookup('foo.bar', { foo: { bar: 1 } }), 1)
32+
t.end()
33+
})
34+
35+
test('numeric path segment is an index', t => {
36+
t.equal(evalLookup('0.name', [{ name: 'a' }]), 'a')
37+
t.end()
38+
})
39+
40+
test('$body is the raw body', t => {
41+
t.match(buildLookup('$body'), /response\.body/)
42+
t.end()
43+
})
44+
45+
test('quote and slash in a key stay valid js', t => {
46+
t.equal(buildLookup("foo.bar'baz"), 'response.body?.["foo"]?.["bar\'baz"]')
47+
t.equal(evalLookup("foo.bar'baz", { foo: { "bar'baz": 2 } }), 2)
48+
assertValidJs(t, 'logs\\.otel.enabled')
49+
t.end()
50+
})
51+
52+
test('adversarial path segments stay valid js', t => {
53+
assertValidJs(t, '../')
54+
assertValidJs(t, '?#')
55+
t.equal(evalLookup('?#', { '?#': 4 }), 4)
56+
t.same(evalLookup('', { x: 1 }), { x: 1 })
57+
t.end()
58+
})

0 commit comments

Comments
 (0)