vue-html-secure
Version:
Vue.js 2.x and 3.x plugin to add HTML secure directives v-html-remove, v-html-escape, v-html-safe
165 lines (134 loc) • 6.01 kB
JavaScript
var doc = document.implementation.createHTMLDocument('');
var div = doc.createElement('div');
function removeHTML(htmlString) {
div.innerHTML = htmlString;
var elements = div.querySelectorAll('*')
while (elements.length) {
div.innerHTML = div.innerText;
elements = div.querySelectorAll('*');
}
return div.innerText;
}
function escapeHTML(htmlString) {
try {
return htmlString.toString().replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
}
catch(e) {
return '';
}
}
function safeHTML(htmlString) {
div.innerHTML = htmlString;
for (var elements = div.querySelectorAll('*'), i = elements.length - 1; i >= 0; i--) {
var element = elements[i],
tagName = element.localName;
if (tagName == 'script' || tagName == 'noscript' || tagName == 'noembed' || !(element.attributes instanceof NamedNodeMap)) {
try {
element.parentNode.removeChild(element);
}
catch(e) {
element.outerHTML = '';
}
continue;
}
if (!element.hasAttributes())
continue;
for (var attributes = element.attributes, j = attributes.length - 1; j >= 0; j--) {
var attribute = attributes[j],
attributeName = attribute.localName,
attributeValue = attribute.value.replace(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205f\u3000]/g, '').toLowerCase().trim();
// Remove insecure attribute starting "on*" (example: <img src='' onerror=alert(1)>)
if (attributeName.indexOf('on') == 0)
element.removeAttribute(attributeName);
// Remove insecure src/href attribute with value starting "javascript:*" (example: href="javascript:alert(1)")
else if ((attributeName == 'src' || attributeName == 'href') && attributeValue.indexOf('javascript:') == 0)
element.removeAttribute(attributeName);
// For non-specific tags remove insecure src/data attribute with value starting "data:*" (example: <embed src="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==">)
else if (['audio', 'image', 'img', 'source', 'video'].indexOf(tagName) == -1 && (attributeName == 'src' || attributeName == 'data') && attributeValue.indexOf('data:') == 0)
element.removeAttribute(attributeName);
}
}
return div.innerHTML;
}
// Export as VUE.js plugin with directives v-html-remove, v-html-escape, v-html-safe
// Usage example:
// import VueSecureHTML from 'VueSecureHTML.js';
// Vue.use(VueSecureHTML);
// <teplate><div v-html-remove="message"></div><div v-html-escape="message"></div><div v-html-safe="message"></div></teplate>
// See directive hooks difference between Vue 2.x and Vue 3.x: https://v3-migration.vuejs.org/breaking-changes/custom-directives.html
module.exports = {
install : function (Vue, options) {
Vue.directive('html-remove', {
// For Vue 2.x
inserted : function (el, binding) {
el.innerHTML = removeHTML(binding.value);
},
// For Vue 3.x
mounted : function (el, binding) {
el.innerHTML = removeHTML(binding.value);
},
// For Vue 2.x
update : function (el, binding) {
if (binding.value !== binding.oldValue)
el.innerHTML = removeHTML(binding.value);
},
// For Vue 3.x
updated : function (el, binding) {
if (binding.value !== binding.oldValue)
el.innerHTML = removeHTML(binding.value);
},
});
Vue.directive('html-escape', {
// For Vue 2.x
inserted : function (el, binding) {
el.innerHTML = escapeHTML(binding.value);
},
// For Vue 3.x
mounted : function (el, binding) {
el.innerHTML = escapeHTML(binding.value);
},
// For Vue 2.x
update : function (el, binding) {
if (binding.value !== binding.oldValue)
el.innerHTML = escapeHTML(binding.value);
},
// For Vue 3.x
updated : function (el, binding) {
if (binding.value !== binding.oldValue)
el.innerHTML = escapeHTML(binding.value);
},
});
Vue.directive('html-safe', {
// For Vue 2.x
inserted : function (el, binding) {
el.innerHTML = safeHTML(binding.value);
},
// For Vue 3.x
mounted : function (el, binding) {
el.innerHTML = safeHTML(binding.value);
},
// For Vue 2.x
update : function (el, binding) {
if (binding.value !== binding.oldValue)
el.innerHTML = safeHTML(binding.value);
},
// For Vue 3.x
updated : function (el, binding) {
if (binding.value !== binding.oldValue)
el.innerHTML = safeHTML(binding.value);
},
});
}
}
// Also export separate functions
// Usage example:
// import VueSecureHTML from 'VueSecureHTML.js';
// Vue.prototype.$removeHTML = VueSecureHTML.removeHTML;
// Vue.prototype.$escapeHTML = VueSecureHTML.escapeHTML;
// Vue.prototype.$safeHTML = VueSecureHTML.safeHTML;
// A) <teplate><div>{{ $removeHTML(message) }}</div><div>{{ $escapeHTML(message) }}</div><div>{{ $safeHTML(message) }}</div></teplate>
// B) <script>var message = 'My <b>secure part</b> and user-provided insecure part: ' + $removeHTML("<img src='' onerror=alert('XSS!')>");</script> + <teplate><div v-html="message"></div></teplate>
module.exports.removeHTML = removeHTML;
module.exports.escapeHTML = escapeHTML;
module.exports.safeHTML = safeHTML;
;