Skip to content

Commit

Permalink
Merge pull request #525 from metrico/fix/security_issues_4_9
Browse files Browse the repository at this point in the history
fix: Incomplete HTML attribute sanitization
  • Loading branch information
jacovinus authored Sep 5, 2024
2 parents d9e96e5 + 85eaba7 commit a2a6bd1
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 63 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ server
.env
*.cookie
packages/main/.env
codeql-results
codeqldb
1 change: 0 additions & 1 deletion packages/main/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
"javascript-time-ago": "^2.5.10",
"jquery": "^3.7.1",
"jsdom": "24.1.1",
"json-markup": "^1.1.4",
"lodash": "^4.17.21",
"lru-memoize": "^1.1.0",
"memoize-one": "^6.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import jsonMarkup from "json-markup";
import jsonMarkup from "../../utils/jsonMarkup";
import React from 'react';


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// this is a fixed version of the json-markup npm package
'use strict'

const INDENTATION = ' '

function createInlineStyle(rules: Record<string, string> | undefined): string {
let inlineStyle = ''
rules && Object.keys(rules).forEach((property) => {
inlineStyle += `${property}:${rules[property]};`
})
return inlineStyle
}

function createStyleApplier(styleFile: Record<string, Record<string, string>> | undefined) {
function applyClass(cssClassName: string): string {
return `class="${cssClassName}"`
}

function applyInlineStyle(cssClassName: string): string {
return `style="${createInlineStyle(styleFile?.['.' + cssClassName])}"`
}

return styleFile ? applyInlineStyle : applyClass
}

function determineType(value: any): string {
if (value === null) return 'null'
if (Array.isArray(value)) return 'array'
if (typeof value === 'string' && /^https?:/.test(value)) return 'link'
if (typeof value === 'object' && typeof value.toISOString === 'function') return 'date'

return typeof value
}

function escapeHtml(unsafeString: string): string {
return unsafeString
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
}

export default function renderJsonMarkup(
jsonObject: any,
styleFile?: Record<string, Record<string, string>> | undefined
): string {
let currentIndentation = ''
const applyStyle = createStyleApplier(styleFile)

function renderList(
items: any[],
startDelimiter: string,
endDelimiter: string,
renderItem: (item: any) => string
): string {
if (!items.length) return `${startDelimiter} ${endDelimiter}`

let result = startDelimiter + '\n'

currentIndentation += INDENTATION
items.forEach((item, index) => {
result += currentIndentation + renderItem(item) + (index < items.length - 1 ? ',' : '') + '\n'
})
currentIndentation = currentIndentation.slice(0, -INDENTATION.length)

return result + currentIndentation + endDelimiter
}

function renderValue(value: any): string {
if (value === undefined) return ''

switch (determineType(value)) {
case 'boolean':
return `<span ${applyStyle('json-markup-bool')}>${value}</span>`

case 'number':
return `<span ${applyStyle('json-markup-number')}>${value}</span>`

case 'date':
return `<span class="json-markup-string">"${escapeHtml(value.toISOString())}"</span>`

case 'null':
return `<span ${applyStyle('json-markup-null')}>null</span>`

case 'string':
return `<span ${applyStyle('json-markup-string')}>"${escapeHtml(value.replace(/\n/g, '\n' + currentIndentation))}"</span>`

case 'link':
return `<span ${applyStyle('json-markup-string')}>"<a href="${escapeHtml(value)}">${escapeHtml(value)}</a>"</span>`

case 'array':
return renderList(value, '[', ']', renderValue)

case 'object':
const keys = Object.keys(value).filter((key) => value[key] !== undefined)

return renderList(keys, '{', '}', (key) =>
`<span ${applyStyle('json-markup-key')}>"${escapeHtml(key)}":</span> ${renderValue(value[key])}`
)
}

return ''
}

return `<div ${applyStyle('json-markup')}>${renderValue(jsonObject)}</div>`
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@ declare module 'combokeys' {
}

declare module 'react-helmet';
declare module 'json-markup';
//declare module 'json-markup';
declare module 'react-vis-force';
declare module 'tween-functions';
4 changes: 2 additions & 2 deletions packages/main/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@ let configOpts = {
"@microlink/react-json-view",
"date-fns",
"nanoid",
"prismjs",
"javascript-time-ago",
"json-markup",
],
prismJs: ["prismjs"],
dayJs: ["dayjs"],
reactDnd: ["react-dnd", "react-dnd-html5-backend"],
memoize: [
"memoize-one",
Expand Down
60 changes: 2 additions & 58 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit a2a6bd1

Please sign in to comment.