TraceView: Add key and url escaping of json tag values (#64331)

This commit is contained in:
Andrej Ocenas 2023-03-08 11:20:08 +01:00 committed by GitHub
parent b093439b2e
commit c7a1216cf6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 160 additions and 2 deletions

View File

@ -24,7 +24,7 @@ const data = [
{ key: 'jsonkey', value: JSON.stringify({ hello: 'world' }) },
];
const setup = (propOverrides?: KeyValuesTableProps) => {
const setup = (propOverrides?: Partial<KeyValuesTableProps>) => {
const props = {
data: data,
...propOverrides,
@ -89,4 +89,28 @@ describe('KeyValuesTable tests', () => {
expect(screen.getAllByRole('button')).toHaveLength(4);
});
it('renders a link in json and properly escapes it', () => {
setup({
data: [
{ key: 'jsonkey', value: JSON.stringify({ hello: 'https://example.com"id=x tabindex=1 onfocus=alert(1)' }) },
],
});
const link = screen.getByText(/https:\/\/example.com/);
expect(link.tagName).toBe('A');
expect(link.attributes.getNamedItem('href')?.value).toBe(
'https://example.com%22id=x%20tabindex=1%20onfocus=alert(1)'
);
});
it('properly escapes json values', () => {
setup({
data: [
{ key: 'jsonkey', value: JSON.stringify({ '<img src=x onerror=alert(1)>': '<img src=x onerror=alert(1)>' }) },
],
});
const values = screen.getAllByText(/onerror=alert/);
expect(values[0].innerHTML).toBe('"&lt;img src=x onerror=alert(1)&gt;":');
expect(values[1].innerHTML).toBe('"&lt;img src=x onerror=alert(1)&gt;"');
});
});

View File

@ -14,7 +14,6 @@
import { css } from '@emotion/css';
import cx from 'classnames';
import jsonMarkup from 'json-markup';
import * as React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
@ -25,6 +24,8 @@ import CopyIcon from '../../common/CopyIcon';
import { TraceKeyValuePair, TraceLink, TNil } from '../../types';
import { ubInlineBlock, uWidth100 } from '../../uberUtilityStyles';
import jsonMarkup from './jsonMarkup';
const copyIconClassName = 'copyIcon';
export const getStyles = (theme: GrafanaTheme2) => {

View File

@ -0,0 +1,133 @@
// The MIT License (MIT)
//
// Copyright (c) 2014 Mathias Buus
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
const INDENT = ' ';
function inlineRule(objRule) {
let str = '';
objRule &&
Object.keys(objRule).forEach(function (rule) {
str += rule + ':' + objRule[rule] + ';';
});
return str;
}
function Stylize(styleFile) {
function styleClass(cssClass) {
return 'class="' + cssClass + '"';
}
function styleInline(cssClass) {
return 'style="' + inlineRule(styleFile['.' + cssClass]) + '"';
}
if (!styleFile) {
return styleClass;
}
return styleInline;
}
function type(doc) {
if (doc === null) {
return 'null';
}
if (Array.isArray(doc)) {
return 'array';
}
if (typeof doc === 'string' && /^https?:/.test(doc)) {
return 'link';
}
if (typeof doc === 'object' && typeof doc.toISOString === 'function') {
return 'date';
}
return typeof doc;
}
function escape(str) {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
module.exports = function (doc, styleFile) {
let indent = '';
const style = Stylize(styleFile);
let forEach = function (list, start, end, fn) {
if (!list.length) {
return start + ' ' + end;
}
let out = start + '\n';
indent += INDENT;
list.forEach(function (key, i) {
out += indent + fn(key) + (i < list.length - 1 ? ',' : '') + '\n';
});
indent = indent.slice(0, -INDENT.length);
return out + indent + end;
};
function visit(obj) {
if (obj === undefined) {
return '';
}
switch (type(obj)) {
case 'boolean':
return '<span ' + style('json-markup-bool') + '>' + obj + '</span>';
case 'number':
return '<span ' + style('json-markup-number') + '>' + obj + '</span>';
case 'date':
return '<span class="json-markup-string">"' + escape(obj.toISOString()) + '"</span>';
case 'null':
return '<span ' + style('json-markup-null') + '>null</span>';
case 'string':
return '<span ' + style('json-markup-string') + '>"' + escape(obj.replace(/\n/g, '\n' + indent)) + '"</span>';
case 'link':
return (
'<span ' + style('json-markup-string') + '>"<a href="' + encodeURI(obj) + '">' + escape(obj) + '</a>"</span>'
);
case 'array':
return forEach(obj, '[', ']', visit);
case 'object':
const keys = Object.keys(obj).filter(function (key) {
return obj[key] !== undefined;
});
return forEach(keys, '{', '}', function (key) {
return '<span ' + style('json-markup-key') + '>"' + escape(key) + '":</span> ' + visit(obj[key]);
});
}
return '';
}
return '<div ' + style('json-markup') + '>' + visit(doc) + '</div>';
};