From 73091fa92e79de84c9568bc6b334f7655cb77ff5 Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Wed, 1 Feb 2023 17:43:32 +0100 Subject: [PATCH] Cherry pick 8b53b44 --- .../SpanDetail/KeyValuesTable.test.js | 24 ++++ .../SpanDetail/KeyValuesTable.tsx | 3 +- .../SpanDetail/jsonMarkup.js | 133 ++++++++++++++++++ 3 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/jsonMarkup.js diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/KeyValuesTable.test.js b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/KeyValuesTable.test.js index 2b5d018f415..b5c738c1371 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/KeyValuesTable.test.js +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/KeyValuesTable.test.js @@ -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({ '': '' }) }, + ], + }); + const values = screen.getAllByText(/onerror=alert/); + expect(values[0].innerHTML).toBe('"<img src=x onerror=alert(1)>":'); + expect(values[1].innerHTML).toBe('"<img src=x onerror=alert(1)>"'); + }); }); diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx index 42ed0bb9855..54e936a59d9 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx @@ -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'; @@ -26,6 +25,8 @@ import { TNil } from '../../types'; import { TraceKeyValuePair, TraceLink } from '../../types/trace'; import { ubInlineBlock, uWidth100 } from '../../uberUtilityStyles'; +import jsonMarkup from './jsonMarkup'; + const copyIconClassName = 'copyIcon'; export const getStyles = (theme: GrafanaTheme2) => { diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/jsonMarkup.js b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/jsonMarkup.js new file mode 100644 index 00000000000..2dcbb1ee895 --- /dev/null +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/jsonMarkup.js @@ -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, '&').replace(//g, '>').replace(/"/g, '"'); +} + +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 '' + obj + ''; + + case 'number': + return '' + obj + ''; + + case 'date': + return '"' + escape(obj.toISOString()) + '"'; + + case 'null': + return 'null'; + + case 'string': + return '"' + escape(obj.replace(/\n/g, '\n' + indent)) + '"'; + + case 'link': + return ( + '"' + escape(obj) + '"' + ); + + 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 '"' + escape(key) + '": ' + visit(obj[key]); + }); + } + + return ''; + } + + return '
' + visit(doc) + '
'; +};