feat(core): add directive for escaping html contents#2951
feat(core): add directive for escaping html contents#2951
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new v-safe-html directive to @kong-ui-public/core to sanitize raw HTML bindings using DOMPurify, along with tests and documentation.
Changes:
- Introduce
vSafeHtmldirective +SafeHtmlPluginfor global registration. - Add unit tests for sanitization behavior and DOMPurify configuration passthrough.
- Add DOMPurify and Vue Test Utils dependencies, plus directive documentation.
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Locks new deps for packages/core/core (dompurify, @vue/test-utils). |
| packages/core/core/src/index.ts | Exposes SafeHtmlPlugin and vSafeHtml from the package entrypoint. |
| packages/core/core/src/directives/vSafeHtml.ts | Implements directive that sanitizes and sets innerHTML. |
| packages/core/core/src/directives/vSafeHtml.spec.ts | Adds Vitest coverage for sanitization + config behavior + updates. |
| packages/core/core/src/directives/README.md | Documents directive registration and usage. |
| packages/core/core/package.json | Adds dompurify dependency and @vue/test-utils devDependency. |
| packages/core/core/README.md | Adds a top-level section documenting v-safe-html. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
packages/core/core/README.md
Outdated
| HTML sanitization directive backed by DOMPurify. | ||
|
|
||
| Global registration: | ||
|
|
||
| ```ts | ||
| import { createApp } from 'vue' | ||
| import { SafeHtmlPlugin } from '@kong-ui-public/core' | ||
|
|
||
| createApp(App).use(SafeHtmlPlugin) | ||
| ``` | ||
|
|
||
| Local registration: | ||
|
|
||
| ```ts | ||
| import { vSafeHtml } from '@kong-ui-public/core' | ||
|
|
||
| export default { | ||
| directives: { safeHtml: vSafeHtml }, | ||
| } | ||
| ``` | ||
|
|
||
| Template usage: | ||
|
|
||
| ```vue | ||
| <div v-safe-html="htmlString" /> | ||
| <div v-safe-html="{ html: htmlString, config: { ALLOWED_TAGS: ['a', 'strong'] } }" /> | ||
| ``` |
There was a problem hiding this comment.
This new section duplicates the directive docs that also live under ./src/directives/README.md, while the rest of this README links out to per-feature docs (e.g., useAxios, useWindow). To keep documentation consistent and avoid drift, consider replacing this block with a link to the directives README (or ensure only one source of truth).
| HTML sanitization directive backed by DOMPurify. | |
| Global registration: | |
| ```ts | |
| import { createApp } from 'vue' | |
| import { SafeHtmlPlugin } from '@kong-ui-public/core' | |
| createApp(App).use(SafeHtmlPlugin) | |
| ``` | |
| Local registration: | |
| ```ts | |
| import { vSafeHtml } from '@kong-ui-public/core' | |
| export default { | |
| directives: { safeHtml: vSafeHtml }, | |
| } | |
| ``` | |
| Template usage: | |
| ```vue | |
| <div v-safe-html="htmlString" /> | |
| <div v-safe-html="{ html: htmlString, config: { ALLOWED_TAGS: ['a', 'strong'] } }" /> | |
| ``` | |
| [v-safe-html directive docs](./src/directives/README.md) |
packages/core/core/README.md
Outdated
| import { vSafeHtml } from '@kong-ui-public/core' | ||
|
|
||
| export default { | ||
| directives: { safeHtml: vSafeHtml }, |
There was a problem hiding this comment.
The code block uses a tab indentation before directives:; elsewhere the repo generally uses spaces. Please convert this to spaces to avoid inconsistent formatting and potential lint/formatting churn.
| directives: { safeHtml: vSafeHtml }, | |
| directives: { safeHtml: vSafeHtml }, |
|
|
||
| expect(wrapper.html()).toContain('<strong>two</strong>') | ||
| expect(wrapper.html()).not.toContain('one') | ||
| }) |
There was a problem hiding this comment.
Current tests cover updates when the directive value is a new string, but they don't cover the object form ({ html, config }) updating—especially the common case where html is mutated while the object reference stays the same. Adding a test for object-value updates would guard against stale DOM issues in the directive updated hook.
| }) | |
| }) | |
| it('updates when object content html changes', async () => { | |
| const content = { | |
| html: '<strong>one</strong>', | |
| config: { ALLOWED_TAGS: ['strong'] }, | |
| } | |
| const wrapper = mount(TestComponent, { | |
| props: { | |
| content, | |
| }, | |
| }) | |
| expect(wrapper.html()).toContain('<strong>one</strong>') | |
| content.html = '<strong>two</strong>' | |
| await wrapper.setProps({ content }) | |
| expect(wrapper.html()).toContain('<strong>two</strong>') | |
| expect(wrapper.html()).not.toContain('one') | |
| }) |
| if (binding.value !== binding.oldValue) { | ||
| applySafeHtml(el, binding.value) | ||
| } |
There was a problem hiding this comment.
The updated hook skips re-sanitizing when binding.value === binding.oldValue, which breaks updates for reactive/object values that are mutated in place (same reference, different html/config). Consider always calling applySafeHtml on updated, or compare the resolved html/config instead of reference equality so DOM stays in sync.
| if (binding.value !== binding.oldValue) { | |
| applySafeHtml(el, binding.value) | |
| } | |
| applySafeHtml(el, binding.value) |
|
I appreciate you updating the description, but I'm still not sure I understand why we need this directive, or where it will be used (meaning I don't see an existing feature or JIRA that needs to take advantage of this functionality). Also, exposing the config to allow-list HTML elements, such as |
We had some occurrences of XSS exploits in the past such as:
The directive should be the very first step that we always want to stay away from using
Agreed with the |
The two instances you linked above wouldn't have been solved by this directive 🤔 I'm still having a hard time understanding why this is needed, as it seems just not utilizing v-html altogether would be the better route. We could add a rule in our shared ESLint config to disallow the attribute (this could be overridden if absolutely needed). |
Summary
DOMPurifyas the escaping method.Reason
A Vue directive for safely rendering HTML content with XSS protection via DOMPurify. Use this instead of
v-htmlto prevent Cross-Site Scripting (XSS) vulnerabilities.Why?:
v-htmlrenders raw HTML without sanitization, which can execute malicious scripts.v-safe-htmlautomatically removes dangerous content (scripts, event handlers, javascript: URLs) while preserving safe HTML formatting.Comparison
v-html(Unsafe)v-safe-html(Safe)<strong>Bold text</strong><a href="https://example.com">Link</a><script>alert('XSS')</script><img src=x onerror="alert('XSS')">onerrorremoved<a href="javascript:alert('XSS')">Click</a>hrefsanitized<div onclick="alert('XSS')">Text</div>onclickremoved<iframe src="evil.com"></iframe>iframeremoved<em>Italic</em> with <br> breaksPackage size impacts (summarized by Copilot)
Current size
@kong-ui-public/core Bundle Impact
Current Bundle Sizes (built Mar 2, before DOMPurify):
ES module: 68 KB (18.8 KB gzipped)
UMD: 48 KB (16.1 KB gzipped)
After the adoption
Expected Impact:
The core package will add 23 KB minified (8.5 KB gzipped) when DOMPurify is included
Well within the 246 KB configured size limit
In production with compression, the impact is only 8.5 KB
Size Analysis
The security benefit of preventing XSS attacks far outweighs the small bundle size impact:
8.5 KB gzipped is negligible for modern web apps
DOMPurify is battle-tested and widely used (millions of downloads/week)
Tree-shaking ensures only used parts of DOMPurify are included
The alternative (custom sanitization) would likely be similar or larger in size and less secure
Verdict: The bundle impact is minimal and acceptable for the critical XSS protection provided.
Expected size:
ES module: 91 KB (~27.3 KB gzipped)
UMD: 71 KB (~24.6 KB gzipped)
And because this will add approximately 8.5KB to the consuming apps that uses
@kong-ui-public/core, if that is not acceptable we could also publish a dedicated package for this work.