Skip to content

Instantly share code, notes, and snippets.

@saeedvir
Last active September 27, 2025 03:01
Show Gist options
  • Save saeedvir/d9b80ff857481dcce26b7610442dfb3f to your computer and use it in GitHub Desktop.
Save saeedvir/d9b80ff857481dcce26b7610442dfb3f to your computer and use it in GitHub Desktop.
secured and improved version of strip_tags php function in javascript
/*
php version : https://gist.github.com/saeedvir/f00d76c33a95c1fbecb297df4a286ebc
*/
function strip_tags(input, allowed) {
if (input == null || input === '') return '';
// Convert to string and handle non-string inputs safely
const str = String(input);
// Parse allowed tags into a Set for faster lookup
const allowedTags = new Set();
if (allowed) {
const matches = String(allowed).toLowerCase().match(/<([a-z][a-z0-9]*)\b[^>]*>/g) || [];
matches.forEach(tag => {
const tagName = tag.match(/<([a-z][a-z0-9]*)/i);
if (tagName && tagName[1]) {
allowedTags.add(tagName[1].toLowerCase());
}
});
}
// Improved regex patterns
const tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi;
const commentsAndPhpTags = /<!--[\s\S]*?-->|<\?(?:php)?[\s\S]*?\?>/gi;
const scriptStyleTags = /<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>/gi;
// Remove dangerous content first
let result = str.replace(commentsAndPhpTags, '')
.replace(scriptStyleTags, '');
// Process remaining tags
result = result.replace(tags, function ($0, $1) {
if (!$1) return ''; // No tag name found
const tagName = $1.toLowerCase();
const isAllowed = allowedTags.has(tagName);
if (!isAllowed) return '';
// Secure the allowed tag by removing dangerous attributes
return secureTag($0, tagName);
});
return result;
}
// Helper function to secure allowed tags by removing dangerous attributes
function secureTag(tag, tagName) {
// Self-closing tags don't need processing
if (tag.endsWith('/>')) {
return removeDangerousAttributes(tag, tagName);
}
// For opening tags, remove dangerous attributes
if (tag.startsWith('</')) {
return tag; // Closing tags are safe
}
return removeDangerousAttributes(tag, tagName);
}
function removeDangerousAttributes(tag, tagName) {
// List of dangerous attributes and protocols
const dangerousAttrs = [
'onabort', 'onactivate', 'onafterprint', 'onafterupdate', 'onbeforeactivate',
'onbeforecopy', 'onbeforecut', 'onbeforedeactivate', 'onbeforeeditfocus',
'onbeforepaste', 'onbeforeprint', 'onbeforeunload', 'onbeforeupdate',
'onblur', 'onbounce', 'oncellchange', 'onchange', 'onclick', 'oncontextmenu',
'oncontrolselect', 'oncopy', 'oncut', 'ondataavailable', 'ondatasetchanged',
'ondatasetcomplete', 'ondblclick', 'ondeactivate', 'ondrag', 'ondragend',
'ondragenter', 'ondragleave', 'ondragover', 'ondragstart', 'ondrop',
'onerror', 'onerrorupdate', 'onfilterchange', 'onfinish', 'onfocus',
'onfocusin', 'onfocusout', 'onhelp', 'onkeydown', 'onkeypress', 'onkeyup',
'onlayoutcomplete', 'onload', 'onlosecapture', 'onmousedown', 'onmouseenter',
'onmouseleave', 'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup',
'onmousewheel', 'onmove', 'onmoveend', 'onmovestart', 'onpaste', 'onpropertychange',
'onreadystatechange', 'onreset', 'onresize', 'onresizeend', 'onresizestart',
'onrowenter', 'onrowexit', 'onrowsdelete', 'onrowsinserted', 'onscroll',
'onselect', 'onselectionchange', 'onselectstart', 'onstart', 'onstop',
'onsubmit', 'onunload', 'fscommand', 'seeksegmenttime'
];
const dangerousProtocols = ['javascript:', 'vbscript:', 'data:', 'livescript:'];
// Remove event handlers and dangerous attributes
let safeTag = tag.replace(/\s+([a-z][a-z0-9_-]*)\s*=\s*("[^"]*"|'[^']*'|[^"'\s>]*)/gi,
function (match, attrName, attrValue) {
attrName = attrName.toLowerCase();
// Remove event handlers
if (dangerousAttrs.includes(attrName)) {
return '';
}
// Remove dangerous protocols from href, src, action, etc.
if (attrName === 'href' || attrName === 'src' || attrName === 'action' || attrName === 'background') {
const value = attrValue.replace(/^["']|["']$/g, '').toLowerCase();
if (dangerousProtocols.some(proto => value.startsWith(proto))) {
return '';
}
}
// Remove style attribute with dangerous expressions
if (attrName === 'style') {
const styleValue = attrValue.replace(/^["']|["']$/g, '');
if (/(expression|javascript|vbscript|url\s*\([^)]*javascript:)/i.test(styleValue)) {
return '';
}
}
return match;
});
// Remove any remaining event handlers that might have been missed
dangerousAttrs.forEach(attr => {
const regex = new RegExp(`\\s+${attr}\\s*=\\s*("[^"]*"|'[^']*'|[^"'\s>]*)`, 'gi');
safeTag = safeTag.replace(regex, '');
});
return safeTag;
}
// Enhanced version with additional security features
function strip_tags_secure(input, allowed, options = {}) {
const defaults = {
encodeUnsafeChars: true,
removeEmptyTags: true,
preserveContent: true
};
const config = { ...defaults, ...options };
let result = strip_tags(input, allowed);
// Additional security measures
if (config.encodeUnsafeChars) {
// Encode potentially dangerous characters that could be used in XSS
result = result.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;')
.replace(/\//g, '&#x2F;');
}
if (config.removeEmptyTags) {
// Remove empty tags that might be used for injection
result = result.replace(/<([a-z][a-z0-9]*)([^>]*)\s*>\s*<\/\1>/gi, '');
}
return result;
}
// Basic usage (same as original)
strip_tags('<p>Hello <b>world</b></p>', '<p><b>');
// Secure version with additional protection
strip_tags_secure(
'<p onclick="alert(\'xss\')">Hello <img src="javascript:alert(1)"></p>',
'<p><img>',
{ encodeUnsafeChars: true }
);
// Test against common XSS vectors
const xssTests = [
'<script>alert("xss")</script>',
'<img src="x" onerror="alert(1)">',
'<p style="expression(alert(1))">Test</p>',
'<a href="javascript:alert(1)">Click</a>'
];
xssTests.forEach(test => {
console.log(strip_tags(test, '<p><img><a>'));
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment