mirror of
https://github.com/lnbits/lnbits.git
synced 2026-08-13 12:42:47 +02:00
fix: sanitize details_link (#4108)
Co-authored-by: alan <alan@lnbits.com>
This commit is contained in:
parent
0a7da6dc73
commit
8b0413fa16
4 changed files with 116 additions and 4 deletions
2
lnbits/static/bundle-components.min.js
vendored
2
lnbits/static/bundle-components.min.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -26,6 +26,7 @@ window.PageExtensions = {
|
|||
selectedExtension: null,
|
||||
selectedImage: null,
|
||||
selectedExtensionDetails: null,
|
||||
selectedExtensionDetailsDescription: '',
|
||||
selectedExtensionRepos: null,
|
||||
selectedRelease: null,
|
||||
permissionGrant: {
|
||||
|
|
@ -656,6 +657,7 @@ window.PageExtensions = {
|
|||
this.selectedExtension =
|
||||
this.extensions.find(ext => ext.id === extId) || this.selectedExtension
|
||||
this.selectedExtensionDetails = null
|
||||
this.selectedExtensionDetailsDescription = ''
|
||||
this.showExtensionDetailsDialog = true
|
||||
this.slide = 0
|
||||
this.fullscreen = false
|
||||
|
|
@ -667,12 +669,89 @@ window.PageExtensions = {
|
|||
)
|
||||
|
||||
this.selectedExtensionDetails = data
|
||||
this.selectedExtensionDetails.description_md =
|
||||
LNbits.utils.convertMarkdown(data.description_md)
|
||||
this.selectedExtensionDetailsDescription =
|
||||
this.extensionDescriptionDocument(data.description_md)
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
}
|
||||
},
|
||||
extensionDescriptionDocument(markdown) {
|
||||
const source = typeof markdown === 'string' ? markdown : ''
|
||||
const rendered = LNbits.utils.convertMarkdown(source)
|
||||
const parsed = new DOMParser().parseFromString(rendered, 'text/html')
|
||||
|
||||
parsed.body
|
||||
.querySelectorAll(
|
||||
'applet, base, embed, form, frame, iframe, link, meta, object, portal, script'
|
||||
)
|
||||
.forEach(element => element.remove())
|
||||
parsed.body.querySelectorAll('*').forEach(element => {
|
||||
for (const attribute of [...element.attributes]) {
|
||||
const attributeName = attribute.name.toLowerCase()
|
||||
if (
|
||||
attributeName.startsWith('on') ||
|
||||
attributeName === 'srcdoc' ||
|
||||
attributeName === 'xlink:href'
|
||||
) {
|
||||
element.removeAttribute(attribute.name)
|
||||
}
|
||||
}
|
||||
})
|
||||
parsed.body.querySelectorAll('a[href], area[href]').forEach(link => {
|
||||
try {
|
||||
const url = new URL(link.getAttribute('href'), window.location.origin)
|
||||
if (
|
||||
!['http:', 'https:'].includes(url.protocol) ||
|
||||
url.username ||
|
||||
url.password
|
||||
) {
|
||||
link.removeAttribute('href')
|
||||
return
|
||||
}
|
||||
link.setAttribute('href', url.href)
|
||||
link.setAttribute('target', '_blank')
|
||||
link.setAttribute('rel', 'noopener noreferrer')
|
||||
} catch (_error) {
|
||||
link.removeAttribute('href')
|
||||
}
|
||||
})
|
||||
|
||||
const csp = [
|
||||
"default-src 'none'",
|
||||
'img-src https: data:',
|
||||
"style-src 'unsafe-inline'",
|
||||
"script-src 'none'",
|
||||
"script-src-attr 'none'",
|
||||
"base-uri 'none'",
|
||||
"form-action 'none'",
|
||||
"frame-src 'none'",
|
||||
"object-src 'none'"
|
||||
].join('; ')
|
||||
const styles = `
|
||||
:root { color-scheme: light dark; font-family: Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
||||
body { margin: 0; color: CanvasText; background: Canvas; line-height: 1.5; overflow-wrap: anywhere; }
|
||||
img { max-width: 100%; height: auto; }
|
||||
pre { overflow: auto; padding: 0.75rem; background: color-mix(in srgb, CanvasText 8%, Canvas); }
|
||||
code { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; }
|
||||
table { display: block; max-width: 100%; overflow-x: auto; border-collapse: collapse; }
|
||||
th, td { padding: 0.35rem 0.6rem; border: 1px solid color-mix(in srgb, CanvasText 20%, Canvas); }
|
||||
a[href] { color: LinkText; cursor: pointer; }
|
||||
`
|
||||
|
||||
return `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="${csp}">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>${styles}</style>
|
||||
</head>
|
||||
<body>
|
||||
${parsed.body.innerHTML}
|
||||
</body>
|
||||
</html>`
|
||||
},
|
||||
async payAndInstall(release) {
|
||||
try {
|
||||
if ((await this.resolveExtensionPermissionGrant(release)) === null) {
|
||||
|
|
|
|||
|
|
@ -1282,7 +1282,15 @@
|
|||
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-8 q-pr-sm">
|
||||
<div v-html="selectedExtensionDetails.description_md"></div>
|
||||
<iframe
|
||||
ref="extensionDescriptionFrame"
|
||||
:srcdoc="selectedExtensionDetailsDescription"
|
||||
title="Extension description"
|
||||
sandbox="allow-popups allow-popups-to-escape-sandbox"
|
||||
referrerpolicy="no-referrer"
|
||||
loading="lazy"
|
||||
style="width: 100%; min-height: 480px; border: 0"
|
||||
></iframe>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-4 q-pl-sm">
|
||||
<lnbits-extension-rating
|
||||
|
|
|
|||
|
|
@ -104,6 +104,31 @@ def test_extension_install_ui_warns_only_for_python_releases():
|
|||
assert '<q-icon name="info"' in extensions_template
|
||||
|
||||
|
||||
def test_extension_details_render_in_isolated_frame():
|
||||
extensions_page = (ROOT / "lnbits/static/js/pages/extensions.js").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
extensions_template = (ROOT / "lnbits/templates/pages/extensions.vue").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
assert 'v-html="selectedExtensionDetails.description_md"' not in extensions_template
|
||||
assert ':srcdoc="selectedExtensionDetailsDescription"' in extensions_template
|
||||
assert (
|
||||
'sandbox="allow-popups allow-popups-to-escape-sandbox"' in extensions_template
|
||||
)
|
||||
assert 'referrerpolicy="no-referrer"' in extensions_template
|
||||
|
||||
assert "\"default-src 'none'\"" in extensions_page
|
||||
assert "\"script-src 'none'\"" in extensions_page
|
||||
assert "\"script-src-attr 'none'\"" in extensions_page
|
||||
assert "attributeName === 'xlink:href'" in extensions_page
|
||||
assert "['http:', 'https:'].includes(url.protocol)" in extensions_page
|
||||
assert "link.setAttribute('target', '_blank')" in extensions_page
|
||||
assert "link.setAttribute('rel', 'noopener noreferrer')" in extensions_page
|
||||
assert "title: 'Open external link?'" not in extensions_page
|
||||
|
||||
|
||||
def test_wasm_admin_frontend_calls_runtime_limit_and_invocation_endpoints():
|
||||
runtime = (
|
||||
ROOT / "lnbits/static/js/components/admin/lnbits-admin-wasm-runtime.js"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue