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 '