Skip to content

Commit 503899b

Browse files
authored
**secruity**: prevent eval by default in jsonpath-plus dependency (#354)
* added tests for jsonpath * spellcheck fix * docs update * update plugin dependency version
1 parent f804789 commit 503899b

8 files changed

+125
-9
lines changed

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
11
# Changelog
22

3+
## 1.3.6 (2023-05-30)
4+
5+
- **Chore**: Docs update
6+
7+
## 1.3.5 (2023-04-05)
8+
9+
- **Security**: Recently, A third party researcher (Alessio Della Libera of **Snyk Research Team**) discovered and privately disclosed to us a stored XSS vulnerability in the Grafana-maintained `marcusolsson-json-datasource` plugin also known as “JSON API plugin” .
10+
11+
Users with the editor role could perform a stored XSS attack against other viewers, editors, and administrators by including a specially crafted javascript statement in the `field` extractor in queries to the marcusolsson-json-datasource plugin. This resulted in XSS against anyone viewing a panel configured to query the datasource with a malicious query.
12+
13+
This vulnerability worked because the `marcusolsson-json-datasource` plugin uses the `jsonpath-plus` library to evaluate editor-supplied jsonpath expressions. In its default configuration (which we used), this library is an XSS vector, as the JSONPath spec allows for embedded subexpressions, which `jsonpath-plus` implements as arbitrary javascript expressions.
14+
15+
In order to mitigate this vulnerability, we now supply a configuration parameter to `jsonpath-plus` which forbids the evaluation of subexpressions; it is important to note that this change may **break** existing JSONPath queries that rely on filter or eval expressions.
16+
17+
If your dashboards currently rely on JSONPath queries containing subexpressions, there are a few potential migration paths:
18+
19+
1. For simple queries that use subexpressions for indexing/slicing, it may be possible to rewrite the query without a subexpressions for instance `[(@.length-1)]` can also be represented as `[:-1]`.
20+
2. For more complex queries, we suggest switching to the [`jsonata` language](http://docs.jsonata.org/simple), which the plugin also supports. This language has similar features to JSONPath, including support for filter expressions (called “predicates” in the documentation).
21+
3. If changing your existing queries isn’t feasible, the community plugin [“Infinity”](https://grafana.com/grafana/plugins/yesoreyeram-infinity-datasource/) supports JSONPath expressions, including filters and subexpressions if used with the `backend` parser option. Please note that Infinity is community supported plugin.
22+
23+
## 1.3.4 (2023-04-04)
24+
25+
- **Chore**: docs update
26+
327
## 1.3.3 (2023-03-20)
428

529
- **Chore**: dependencies update

cspell.config.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
"./node_modules/",
44
"./.github/",
55
"./dist/",
6+
"coverage",
7+
"./src/**/*.test.ts",
68
"./website/src/css",
79
"./website/*.js",
810
"./website/.docusaurus/",
@@ -11,6 +13,7 @@
1113
"docker-compose.yaml"
1214
],
1315
"words": [
16+
"Alessio",
1417
"amng",
1518
"clsx",
1619
"datasource",
@@ -20,9 +23,12 @@
2023
"jsonata",
2124
"jsonplaceholder",
2225
"Kensington",
26+
"Libera",
2327
"marcusolsson",
2428
"Olsson",
2529
"rejohnst",
30+
"Snyk",
31+
"subexpressions",
2632
"testdata",
2733
"Timestmap",
2834
"Totalus",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "grafana-json-datasource",
3-
"version": "1.3.3",
3+
"version": "1.3.6",
44
"description": "A data source plugin for loading JSON APIs into Grafana",
55
"keywords": [
66
"grafana",

src/datasource.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import { getTemplateSrv } from '@grafana/runtime';
1515
import jsonata from 'jsonata';
1616
import { JSONPath } from 'jsonpath-plus';
17+
import { jp } from './jsonpath';
1718
import _ from 'lodash';
1819
import API from './api';
1920
import { detectFieldType } from './detectFieldType';
@@ -173,7 +174,7 @@ export class JsonDataSource extends DataSourceApi<JsonApiQuery, JsonApiDataSourc
173174
};
174175
default:
175176
const path = replaceWithVars(field.jsonPath);
176-
const values = JSONPath({ path, json });
177+
const values = jp({ path, json });
177178

178179
// Get the path for automatic setting of the field name.
179180
//
@@ -244,9 +245,11 @@ export class JsonDataSource extends DataSourceApi<JsonApiQuery, JsonApiDataSourc
244245
}
245246
}
246247

247-
const replace = (scopedVars?: any, range?: TimeRange) => (str: string): string => {
248-
return replaceMacros(getTemplateSrv().replace(str, scopedVars), range);
249-
};
248+
const replace =
249+
(scopedVars?: any, range?: TimeRange) =>
250+
(str: string): string => {
251+
return replaceMacros(getTemplateSrv().replace(str, scopedVars), range);
252+
};
250253

251254
// replaceMacros substitutes all available macros with their current value.
252255
export const replaceMacros = (str: string, range?: TimeRange) => {

src/jsonpath.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { jp } from './jsonpath';
2+
3+
describe('jsonpath test', () => {
4+
it('sanity test 1', async () => {
5+
const result = jp({
6+
json: [
7+
{ name: 'foo', age: 30 },
8+
{ name: 'bar', age: 17 },
9+
],
10+
path: '$.*.name',
11+
});
12+
expect(result).toStrictEqual(['foo', 'bar']);
13+
});
14+
it('sanity test 2', async () => {
15+
const result = jp({
16+
json: [
17+
{ name: 'foo', age: 30 },
18+
{ name: 'bar', age: 17 },
19+
],
20+
path: '$.0.name',
21+
});
22+
expect(result).toStrictEqual(['foo']);
23+
});
24+
it('sanity test 3', async () => {
25+
const json = {
26+
store: {
27+
book: [
28+
{
29+
category: 'reference',
30+
author: 'Nigel Rees',
31+
title: 'Sayings of the Century',
32+
price: 8.95,
33+
},
34+
{
35+
category: 'fiction',
36+
author: 'Evelyn Waugh',
37+
title: 'Sword of Honour',
38+
price: 12.99,
39+
},
40+
{
41+
category: 'fiction',
42+
author: 'Herman Melville',
43+
title: 'Moby Dick',
44+
isbn: '0-553-21311-3',
45+
price: 8.99,
46+
},
47+
{
48+
category: 'fiction',
49+
author: 'J. R. R. Tolkien',
50+
title: 'The Lord of the Rings',
51+
isbn: '0-395-19395-8',
52+
price: 22.99,
53+
},
54+
],
55+
bicycle: {
56+
color: 'red',
57+
price: 19.95,
58+
},
59+
},
60+
expensive: 10,
61+
};
62+
expect(jp({ json, path: '$.store.book.length' })).toStrictEqual([4]);
63+
});
64+
it('sanity test 4', () => {
65+
expect(() => {
66+
jp({
67+
json: [
68+
{ name: 'foo', age: 30 },
69+
{ name: 'bar', age: 17 },
70+
],
71+
path: `$..[?(@property !== this.constructor.constructor("alert('foo')")();)]`,
72+
});
73+
}).toThrow();
74+
});
75+
});

src/jsonpath.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { JSONPath, JSONPathOptions } from 'jsonpath-plus';
2+
3+
export function jp(options: JSONPathOptions): any {
4+
return JSONPath({
5+
...options,
6+
preventEval: true,
7+
});
8+
}

src/plugin.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@
3939
"updated": "%TODAY%"
4040
},
4141
"dependencies": {
42-
"grafanaDependency": ">=7.3.0",
43-
"grafanaVersion": "7.3.0",
42+
"grafanaDependency": ">=8.5.13",
43+
"grafanaVersion": "8.5.x",
4444
"plugins": []
4545
}
4646
}

src/suggestions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { TypeaheadInput, TypeaheadOutput } from '@grafana/ui';
2-
import { JSONPath } from 'jsonpath-plus';
2+
import { jp } from './jsonpath';
33

44
/**
55
* onSuggest returns suggestions for the current JSON Path and cursor position.
@@ -83,7 +83,7 @@ export const onSuggest = async (input: TypeaheadInput, onData: () => Promise<any
8383
// Get the actual JSON for parsing.
8484
const response = await onData();
8585

86-
const values = JSONPath({ path, json: response });
86+
const values = jp({ path, json: response });
8787

8888
// Don't attempt to suggest if this is a leaf node, e.g. strings, numbers, and booleans.
8989
if (typeof values[0] !== 'object') {

0 commit comments

Comments
 (0)