From 3a7988f734325ec39676427f79141b32dfde0974 Mon Sep 17 00:00:00 2001 From: Mrin Date: Sat, 7 Dec 2024 14:04:57 -0500 Subject: [PATCH] fixes #1061 - santize descriptions to mute the possibility of xss attacks --- package-lock.json | 10 ++++++++ package.json | 1 + src/components/api-request.js | 27 +++++++++++++++----- src/components/api-response.js | 23 +++++++++++++---- src/components/schema-tree.js | 24 +++++++++++------ src/templates/components-template.js | 7 ++++- src/templates/endpoint-template.js | 17 +++++++----- src/templates/expanded-endpoint-template.js | 21 ++++++++++++--- src/templates/focused-endpoint-template.js | 9 ++++++- src/templates/json-schema-viewer-template.js | 5 +++- src/templates/overview-template.js | 16 +++++++++--- src/templates/security-scheme-template.js | 7 ++++- src/templates/server-template.js | 5 +++- 13 files changed, 134 insertions(+), 38 deletions(-) diff --git a/package-lock.json b/package-lock.json index f6db3ccb..e162f547 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@types/js-yaml": "^4.0.9", "base64-arraybuffer": "^1.0.2", "buffer": "^6.0.3", + "dompurify": "^3.2.2", "github-slugger": "^2.0.0", "js-yaml": "^4.1.0", "lit": "^3.2.1", @@ -3192,6 +3193,15 @@ "dev": true, "license": "MIT" }, + "node_modules/dompurify": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.2.tgz", + "integrity": "sha512-YMM+erhdZ2nkZ4fTNRTSI94mb7VG7uVF5vj5Zde7tImgnhZE3R6YW/IACGIHb2ux+QkEXMhe591N+5jWOmL4Zw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/drange": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/drange/-/drange-1.1.1.tgz", diff --git a/package.json b/package.json index d0e9b752..0e6a49a6 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@types/js-yaml": "^4.0.9", "base64-arraybuffer": "^1.0.2", "buffer": "^6.0.3", + "dompurify": "^3.2.2", "github-slugger": "^2.0.0", "js-yaml": "^4.1.0", "lit": "^3.2.1", diff --git a/src/components/api-request.js b/src/components/api-request.js index f20f2514..c0ff5944 100644 --- a/src/components/api-request.js +++ b/src/components/api-request.js @@ -1,5 +1,6 @@ import { LitElement, html, css } from 'lit'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; +import DOMPurify from 'dompurify'; import { guard } from 'lit/directives/guard.js'; import { live } from 'lit/directives/live.js'; import { ifDefined } from 'lit/directives/if-defined.js'; @@ -332,7 +333,9 @@ export default class ApiRequest extends LitElement { (v) => html`
  • ${this.renderExample(v, paramType, paramName)} ${v.summary?.length > 0 ? html`(${v.summary})` : ''} - ${v.description?.length > 0 ? html`

    ${unsafeHTML(marked(v.description))}

    ` : ''} + ${v.description?.length > 0 + ? html`

    ${unsafeHTML(DOMPurify.sanitize(marked(v.description), { USE_PROFILES: { html: true } }))}

    ` + : ''}
  • ` )} `; @@ -607,7 +610,9 @@ export default class ApiRequest extends LitElement { ${this.allowTry === 'true' ? html`` : ''} - ${unsafeHTML(marked(param.description || ''))} + + ${unsafeHTML(DOMPurify.sanitize(marked(param.description || ''), { USE_PROFILES: { html: true } }))} + ${this.exampleListTemplate.call(this, param.name, paramSchema.type, example.exampleList)} @@ -765,7 +770,9 @@ export default class ApiRequest extends LitElement { > ${v.exampleSummary && v.exampleSummary.length > 80 ? html`
    ${v.exampleSummary}
    ` : ''} ${v.exampleDescription - ? html`
    ${unsafeHTML(marked(v.exampleDescription || ''))}
    ` + ? html`
    + ${unsafeHTML(DOMPurify.sanitize(marked(v.exampleDescription || ''), { USE_PROFILES: { html: true } }))} +
    ` : ''}
             ${this.request_body.description
    -          ? html`
    ${unsafeHTML(marked(this.request_body.description))}
    ` + ? html`
    + ${unsafeHTML(DOMPurify.sanitize(marked(this.request_body.description), { USE_PROFILES: { html: true } }))} +
    ` : ''} ${this.selectedRequestBodyType.includes('json') || this.selectedRequestBodyType.includes('xml') || @@ -1161,7 +1170,9 @@ ${v.exampleFormat === 'text' ? v.exampleValue : JSON.stringify(v.exampleValue, n - ${unsafeHTML(marked(fieldSchema.description || ''))} + + ${unsafeHTML(DOMPurify.sanitize(marked(fieldSchema.description || '')), { USE_PROFILES: { html: true } })} + ${this.exampleListTemplate.call(this, fieldName, paramSchema.type, example.exampleList)} @@ -1185,7 +1196,11 @@ ${v.exampleFormat === 'text' ? v.exampleValue : JSON.stringify(v.exampleValue, n .textContent="${exampleValue}" style="width:100%" > - ${schema.description ? html`${unsafeHTML(marked(schema.description))}` : ''} + ${schema.description + ? html` + ${unsafeHTML(DOMPurify.sanitize(marked(schema.description), { USE_PROFILES: { html: true } }))} + ` + : ''} `; } diff --git a/src/components/api-response.js b/src/components/api-response.js index abcf91eb..8de96337 100644 --- a/src/components/api-response.js +++ b/src/components/api-response.js @@ -1,5 +1,6 @@ import { LitElement, html, css } from 'lit'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; +import DOMPurify from 'dompurify'; import { marked } from 'marked'; import { schemaInObjectNotation, generateExample, standardizeExample } from '~/utils/schema-utils'; import FontStyles from '~/styles/font-styles'; @@ -177,7 +178,11 @@ export default class ApiResponse extends LitElement { (status) => html`
    - ${unsafeHTML(marked(this.responses[status]?.description || ''))} + ${unsafeHTML( + DOMPurify.sanitize(marked(this.responses[status]?.description || ''), { USE_PROFILES: { html: true } }) + )} ${this.headersForEachRespStatus[status] && this.headersForEachRespStatus[status]?.length > 0 ? html`${this.responseHeaderListTemplate(this.headersForEachRespStatus[status])}` : ''} @@ -241,7 +246,9 @@ export default class ApiResponse extends LitElement { ${v.schema?.type || ''} -
    ${unsafeHTML(marked(v.description || ''))}
    +
    + ${unsafeHTML(DOMPurify.sanitize(marked(v.description || ''), { USE_PROFILES: { html: true } }))} +
    ${v.schema?.example || ''} @@ -293,7 +300,9 @@ export default class ApiResponse extends LitElement { : ''} ${mimeRespDetails.examples[0].exampleDescription ? html`
    - ${unsafeHTML(marked(mimeRespDetails.examples[0].exampleDescription || ''))} + ${unsafeHTML(DOMPurify.sanitize(marked(mimeRespDetails.examples[0].exampleDescription || '')), { + USE_PROFILES: { html: true }, + })}
    ` : ''} - ${unsafeHTML(marked(mimeRespDetails.examples[0].exampleDescription || ''))} + ${unsafeHTML( + DOMPurify.sanitize(marked(mimeRespDetails.examples[0].exampleDescription || ''), { USE_PROFILES: { html: true } }) + )}
    ` : ''}
    @@ -334,7 +345,9 @@ ${mimeRespDetails.examples[0].exampleValue}
    ${v.exampleSummary && v.exampleSummary.length > 80 ? html`
    ${v.exampleSummary}
    ` : ''} ${v.exampleDescription - ? html`
    ${unsafeHTML(marked(v.exampleDescription || ''))}
    ` + ? html`
    + ${unsafeHTML(DOMPurify.sanitize(marked(v.exampleDescription || ''), { USE_PROFILES: { html: true } }))} +
    ` : ''} ${v.exampleFormat === 'json' ? html`` : ''}
    - ${unsafeHTML(marked(this.data?.['::description'] || ''))} + + ${unsafeHTML(DOMPurify.sanitize(marked(this.data?.['::description'] || ''), { USE_PROFILES: { html: true } }))} ${this.data ? html` ${this.generateTree( this.data['::type'] === 'array' ? this.data['::props'] : this.data, @@ -250,7 +253,9 @@ export default class SchemaTree extends LitElement { : ''} ${openBracket} -
    ${unsafeHTML(marked(description || ''))}
    +
    + ${unsafeHTML(DOMPurify.sanitize(marked(description || ''), { USE_PROFILES: { html: true } }))} +
    ${unsafeHTML( - marked( - dataType === 'array' - ? `${descrExpander} ${description}` - : schemaTitle - ? `${descrExpander} ${schemaTitle}: ${schemaDescription}` - : `${descrExpander} ${schemaDescription}` + DOMPurify.sanitize( + marked( + dataType === 'array' + ? `${descrExpander} ${description}` + : schemaTitle + ? `${descrExpander} ${schemaTitle}: ${schemaDescription}` + : `${descrExpander} ${schemaDescription}` + ), + { USE_PROFILES: { html: true } } ) )} `}` diff --git a/src/templates/components-template.js b/src/templates/components-template.js index 8877faf5..4bb44e86 100644 --- a/src/templates/components-template.js +++ b/src/templates/components-template.js @@ -1,5 +1,6 @@ import { html } from 'lit'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; +import DOMPurify from 'dompurify'; import { marked } from 'marked'; import { schemaInObjectNotation } from '~/utils/schema-utils'; import '~/components/json-tree'; @@ -72,7 +73,11 @@ export default function componentsTemplate() { >
    ${component.name}
    - ${unsafeHTML(`
    ${marked(component.description ? component.description : '')}
    `)} + ${unsafeHTML( + `
    ${DOMPurify.sanitize(marked(component.description || ''), { + USE_PROFILES: { html: true }, + })}
    ` + )}
    diff --git a/src/templates/endpoint-template.js b/src/templates/endpoint-template.js index 0ac56aac..25b7dad7 100644 --- a/src/templates/endpoint-template.js +++ b/src/templates/endpoint-template.js @@ -1,5 +1,6 @@ import { html } from 'lit'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; +import DOMPurify from 'dompurify'; import { marked } from 'marked'; import '~/components/api-request'; import '~/components/api-response'; @@ -125,11 +126,15 @@ function endpointBodyTemplate(path) { ` : ''} ${path.description - ? html`
    ${unsafeHTML(marked(path.description))}
    ` + ? html`
    + ${unsafeHTML(DOMPurify.sanitize(marked(path.description), { USE_PROFILES: { html: true } }))} +
    ` : ''} ${path.externalDocs?.url || path.externalDocs?.description ? html`
    -
    ${unsafeHTML(marked(path.externalDocs?.description || ''))}
    +
    + ${unsafeHTML(DOMPurify.sanitize(marked(path.externalDocs?.description || ''), { USE_PROFILES: { html: true } }))} +
    ${path.externalDocs?.url ? html` + : html`
    Expand all @@ -260,7 +265,7 @@ export default function endpointTemplate(isMini = false, pathsExpanded = false) ` : ''}
    @@ -130,11 +133,15 @@ export function expandedEndpointBodyTemplate(path, tagName = '', tagDescription
    `} `} - ${path.description ? html`
    ${unsafeHTML(marked(path.description))}
    ` : ''} + ${path.description + ? html`
    ${unsafeHTML(DOMPurify.sanitize(marked(path.description), { USE_PROFILES: { html: true } }))}
    ` + : ''} ${pathSecurityTemplate.call(this, path.security)} ${path.externalDocs?.url || path.externalDocs?.description ? html`
    -
    ${unsafeHTML(marked(path.externalDocs?.description || ''))}
    +
    + ${unsafeHTML(DOMPurify.sanitize(marked(path.externalDocs?.description || ''), { USE_PROFILES: { html: true } }))} +
    ${path.externalDocs?.url ? html`
    ${unsafeHTML(`
    - ${marked(tag.description || '', this.infoDescriptionHeadingsInNavBar === 'true' ? { renderer: headingRenderer(tag.elementId) } : undefined)} + ${DOMPurify.sanitize( + marked( + tag.description || '', + this.infoDescriptionHeadingsInNavBar === 'true' ? { renderer: headingRenderer(tag.elementId) } : undefined + ), + { USE_PROFILES: { html: true } } + )}
    `)}
    diff --git a/src/templates/focused-endpoint-template.js b/src/templates/focused-endpoint-template.js index d08a1ecc..75e9c312 100644 --- a/src/templates/focused-endpoint-template.js +++ b/src/templates/focused-endpoint-template.js @@ -1,5 +1,6 @@ import { html } from 'lit'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; +import DOMPurify from 'dompurify'; import { marked } from 'marked'; import Slugger from 'github-slugger'; import { expandedEndpointBodyTemplate } from '~/templates/expanded-endpoint-template'; @@ -41,7 +42,13 @@ function focusedTagBodyTemplate(tag) { ${this.onNavTagClick === 'show-description' && tag.description ? html`
    ${unsafeHTML(`
    - ${marked(tag.description || '', this.infoDescriptionHeadingsInNavBar === 'true' ? { renderer: headingRenderer(tag.elementId) } : undefined)} + ${DOMPurify.sanitize( + marked( + tag.description || '', + this.infoDescriptionHeadingsInNavBar === 'true' ? { renderer: headingRenderer(tag.elementId) } : undefined + ), + { USE_PROFILES: { html: true } } + )}
    `)}
    ` : ''} diff --git a/src/templates/json-schema-viewer-template.js b/src/templates/json-schema-viewer-template.js index a484849a..15afeb2e 100644 --- a/src/templates/json-schema-viewer-template.js +++ b/src/templates/json-schema-viewer-template.js @@ -1,5 +1,6 @@ import { html } from 'lit'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; +import DOMPurify from 'dompurify'; import { marked } from 'marked'; // Templates @@ -70,7 +71,9 @@ function jsonSchemaBodyTemplate() { >
    ${jSchemaBody.name}
    - ${unsafeHTML(marked(jSchemaBody.description || ''))} + ${unsafeHTML(DOMPurify.sanitize(marked(jSchemaBody.description || ''), { USE_PROFILES: { html: true } }))}
    diff --git a/src/templates/overview-template.js b/src/templates/overview-template.js index c4534d48..47c13462 100644 --- a/src/templates/overview-template.js +++ b/src/templates/overview-template.js @@ -1,5 +1,6 @@ import { html } from 'lit'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; +import DOMPurify from 'dompurify'; import Slugger from 'github-slugger'; import { marked } from 'marked'; import { downloadResource, viewResource } from '~/utils/common-utils'; @@ -87,10 +88,17 @@ export default function overviewTemplate() {
    ${this.resolvedSpec.info.description - ? html`${unsafeHTML(` -
    - ${marked(this.resolvedSpec.info.description, this.infoDescriptionHeadingsInNavBar === 'true' ? { renderer: headingRenderer() } : undefined)} -
    `)}` + ? html`${unsafeHTML( + DOMPurify.sanitize( + `
    + ${marked( + this.resolvedSpec.info.description, + this.infoDescriptionHeadingsInNavBar === 'true' ? { renderer: headingRenderer() } : undefined + )} +
    `, + { USE_PROFILES: { html: true } } + ) + )}` : ''}
    ` diff --git a/src/templates/security-scheme-template.js b/src/templates/security-scheme-template.js index 4db86b51..ff50cd20 100644 --- a/src/templates/security-scheme-template.js +++ b/src/templates/security-scheme-template.js @@ -1,5 +1,6 @@ import { html } from 'lit'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; +import DOMPurify from 'dompurify'; import { marked } from 'marked'; const codeVerifier = '731DB1C3F7EA533B85E29492D26AA-1234567890-1234567890'; @@ -598,7 +599,11 @@ export default function securitySchemeTemplate(allowTry = 'true') { ` : ''}
    - ${v.description ? html`
    ${unsafeHTML(marked(v.description || ''))}
    ` : ''} + ${v.description + ? html`
    + ${unsafeHTML(DOMPurify.sanitize(marked(v.description || ''), { USE_PROFILES: { html: true } }))} +
    ` + : ''} ${v.type.toLowerCase() === 'apikey' ? html`
    Send ${v.name} in ${v.in}
    ${allowTry === 'true' diff --git a/src/templates/server-template.js b/src/templates/server-template.js index 2673f27e..cf1c484e 100644 --- a/src/templates/server-template.js +++ b/src/templates/server-template.js @@ -1,5 +1,6 @@ import { html } from 'lit'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; +import DOMPurify from 'dompurify'; import { marked } from 'marked'; export function setApiServer(serverUrl) { @@ -71,7 +72,9 @@ function serverVarsTemplate() { ${kv[1].description ? html` - ${unsafeHTML(marked(kv[1].description))} + + ${unsafeHTML(DOMPurify.sanitize(marked(kv[1].description), { USE_PROFILES: { html: true } }))} + ` : ''}