fix(admin): sanitize the richeditor read-only display with DOMPurify (T-10-16)
- displayHtml uses a dedicated DOMPurify 3.4.16 instance: EDITOR_TAGS, href only on <a> through isAllowedLink, DROPPED elements go with their content - string in, string out: parsed into DOMPurify's inert document, nothing adopted into the live document before binding - fails closed (empty string) unless isSupported and a fixed canary hold - Vitest browser mode in Playwright Chromium (npm run test:browser) covers the clobbering vectors, off-origin form, iframe, meta refresh, links and the inert parse; happy-dom keeps engine-independent assertions
This commit is contained in:
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { enableAutoUnmount, flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||
import type { FormField } from '../../src/api/types'
|
||||
import RichEditorField from '../../src/components/form/fields/RichEditorField.vue'
|
||||
import { displayHtml, isAllowedLink } from '../../src/components/form/richeditor/html'
|
||||
import { isAllowedLink } from '../../src/components/form/richeditor/html'
|
||||
import { isLossy } from '../../src/components/form/richeditor/lossy'
|
||||
import { companionsFormSchemaFixture, langFixture } from '../fixtures/typed'
|
||||
import { API, mountApp, requestsTo, resetState, type Route } from '../helpers'
|
||||
@@ -160,10 +160,16 @@ describe('RichEditorField', () => {
|
||||
controlId: 'f-body',
|
||||
},
|
||||
})
|
||||
// happy-dom cannot run DOMPurify faithfully, so displayHtml binds nothing
|
||||
// here (it fails closed). Exact display assertions live in
|
||||
// tests/browser/richeditor-display.test.ts; these hold in any engine.
|
||||
const box = wrapper.find('[data-richeditor-readonly]')
|
||||
expect(box.find('script').exists()).toBe(false)
|
||||
expect(box.find('p').attributes('onclick')).toBeUndefined()
|
||||
expect(box.findAll('a').map((link) => link.attributes('href'))).toEqual([undefined, '/ok'])
|
||||
expect(box.find('[onclick]').exists()).toBe(false)
|
||||
expect(box.html()).not.toContain('javascript:')
|
||||
for (const link of box.findAll('a')) {
|
||||
expect(isAllowedLink(link.attributes('href'))).toBe(true)
|
||||
}
|
||||
expect((window as unknown as { __re?: number }).__re).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -183,10 +189,6 @@ describe('richeditor policy helpers', () => {
|
||||
expect(isLossy('<p>a</p><H2>b</H2>')).toBe(true)
|
||||
expect(isLossy('')).toBe(false)
|
||||
})
|
||||
|
||||
it('unwraps unknown elements and drops scripts for display', () => {
|
||||
expect(displayHtml('<section><p class="x">a</p></section><script>b</script>')).toBe('<p>a</p>')
|
||||
})
|
||||
})
|
||||
|
||||
// Plan 06 coverage: the value contract, the link popover, the notices
|
||||
|
||||
Reference in New Issue
Block a user