Fix XSS in PersonalAPIKey tooltip (#35174)

This commit is contained in:
Thomas Piccirello
2025-07-17 06:36:22 +02:00
committed by GitHub
parent 9a0099f89b
commit 51552c4000
4 changed files with 66 additions and 22 deletions

View File

@@ -139,7 +139,7 @@
"d3": "^7.9.0",
"d3-sankey": "^0.12.3",
"dayjs": "1.11.11",
"dompurify": "^3.0.6",
"dompurify": "^3.2.6",
"elkjs": "^0.10.0",
"emoji-regex": "^10.4.0",
"eventsource-parser": "^3.0.0",

View File

@@ -2157,3 +2157,10 @@ export function getRelativeNextPath(nextPath: string | null | undefined, locatio
return null
}
}
export function escapeHtml(raw: string): string {
// renders a string safe to use in dangerouslySetInnerHTML
const div = document.createElement('div')
div.textContent = raw
return div.innerHTML
}

View File

@@ -15,13 +15,14 @@ import {
Tooltip,
} from '@posthog/lemon-ui'
import clsx from 'clsx'
import DOMPurify from 'dompurify'
import { useActions, useValues } from 'kea'
import { Form } from 'kea-forms'
import { IconErrorOutline } from 'lib/lemon-ui/icons'
import { LemonButton } from 'lib/lemon-ui/LemonButton'
import { LemonField } from 'lib/lemon-ui/LemonField'
import { API_KEY_SCOPE_PRESETS, API_SCOPES, MAX_API_KEYS_PER_USER } from 'lib/scopes'
import { capitalizeFirstLetter, humanFriendlyDetailedTime } from 'lib/utils'
import { capitalizeFirstLetter, escapeHtml, humanFriendlyDetailedTime } from 'lib/utils'
import { Fragment, useEffect } from 'react'
import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic'
@@ -298,13 +299,25 @@ function PersonalAPIKeysTable(): JSX.Element {
const orgNames = restrictedOrgs.map((org: any) => org.name)
const tooltipMessage =
orgNames.length === 1
? `Organization <strong>${orgNames[0]}</strong> has restricted the use of personal API keys.`
: `Organizations <strong>${orgNames.join(
', '
? `Organization <strong>${escapeHtml(
orgNames[0]
)}</strong> has restricted the use of personal API keys.`
: `Organizations <strong>${escapeHtml(
orgNames.join(', ')
)}</strong> have restricted the use of personal API keys.`
return (
<Tooltip title={<span dangerouslySetInnerHTML={{ __html: tooltipMessage }} />}>
<Tooltip
title={
<span
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(tooltipMessage, {
ALLOWED_TAGS: ['strong'],
}),
}}
/>
}
>
<LemonTag type="danger">Disabled</LemonTag>
</Tooltip>
)
@@ -319,17 +332,21 @@ function PersonalAPIKeysTable(): JSX.Element {
const restrictedOrgNames = restrictedOrgs.map((org: any) => org.name)
if (restrictedOrgNames.length === 1 && restrictedTeamNames.length === 1) {
tooltipMessage = `Organization <strong>${restrictedOrgNames[0]}</strong> has restricted the use of personal API keys. This key will not work for project <strong>${restrictedTeamNames[0]}</strong>.`
} else if (restrictedOrgNames.length === 1) {
tooltipMessage = `Organization <strong>${
tooltipMessage = `Organization <strong>${escapeHtml(
restrictedOrgNames[0]
}</strong> has restricted the use of personal API keys. This key will not work for projects: <strong>${restrictedTeamNames.join(
', '
)}</strong> has restricted the use of personal API keys. This key will not work for project <strong>${escapeHtml(
restrictedTeamNames[0]
)}</strong>.`
} else if (restrictedOrgNames.length === 1) {
tooltipMessage = `Organization <strong>${escapeHtml(
restrictedOrgNames[0]
)}</strong> has restricted the use of personal API keys. This key will not work for projects: <strong>${escapeHtml(
restrictedTeamNames.join(', ')
)}</strong>.`
} else {
// Multiple organizations affecting projects
tooltipMessage = `Multiple organizations have restricted personal API keys. This key will not work for projects: <strong>${restrictedTeamNames.join(
', '
tooltipMessage = `Multiple organizations have restricted personal API keys. This key will not work for projects: <strong>${escapeHtml(
restrictedTeamNames.join(', ')
)}</strong>.`
}
}
@@ -338,16 +355,28 @@ function PersonalAPIKeysTable(): JSX.Element {
const restrictedOrgNames = restrictedOrgs.map((org: any) => org.name)
if (restrictedOrgNames.length === 1) {
tooltipMessage = `Organization <strong>${restrictedOrgNames[0]}</strong> has restricted the use of personal API keys.`
tooltipMessage = `Organization <strong>${escapeHtml(
restrictedOrgNames[0]
)}</strong> has restricted the use of personal API keys.`
} else {
tooltipMessage = `Organizations <strong>${restrictedOrgNames.join(
', '
tooltipMessage = `Organizations <strong>${escapeHtml(
restrictedOrgNames.join(', ')
)}</strong> have restricted the use of personal API keys.`
}
}
return (
<Tooltip title={<span dangerouslySetInnerHTML={{ __html: tooltipMessage }} />}>
<Tooltip
title={
<span
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(tooltipMessage, {
ALLOWED_TAGS: ['strong'],
}),
}}
/>
}
>
<LemonTag type="warning">Partial restrictions</LemonTag>
</Tooltip>
)

18
pnpm-lock.yaml generated
View File

@@ -764,8 +764,8 @@ importers:
specifier: 1.11.11
version: 1.11.11(patch_hash=lbfir4woetqmvzqg7l4q5mjtfq)
dompurify:
specifier: ^3.0.6
version: 3.0.6
specifier: ^3.2.6
version: 3.2.6
elkjs:
specifier: ^0.10.0
version: 0.10.0
@@ -7868,6 +7868,9 @@ packages:
'@types/trusted-types@2.0.4':
resolution: {integrity: sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ==}
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
'@types/unist@2.0.6':
resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==}
@@ -9929,8 +9932,8 @@ packages:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
dompurify@3.0.6:
resolution: {integrity: sha512-ilkD8YEnnGh1zJ240uJsW7AzE+2qpbOUYjacomn3AvJ6J4JhKGSZ2nh4wUIXPZrEPppaCLx5jFe8T89Rk8tQ7w==}
dompurify@3.2.6:
resolution: {integrity: sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==}
domutils@1.7.0:
resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==}
@@ -24815,6 +24818,9 @@ snapshots:
'@types/trusted-types@2.0.4': {}
'@types/trusted-types@2.0.7':
optional: true
'@types/unist@2.0.6': {}
'@types/uuid@10.0.0': {}
@@ -27413,7 +27419,9 @@ snapshots:
dependencies:
domelementtype: 2.3.0
dompurify@3.0.6: {}
dompurify@3.2.6:
optionalDependencies:
'@types/trusted-types': 2.0.7
domutils@1.7.0:
dependencies: