Instantly share code, notes, and snippets.
Last active
September 27, 2025 03:01
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save saeedvir/f00d76c33a95c1fbecb297df4a286ebc to your computer and use it in GitHub Desktop.
secure strip_tags function
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/* | |
javascript version : https://gist.github.com/saeedvir/d9b80ff857481dcce26b7610442dfb3f | |
*/ | |
function strip_tags_secure($input, $allowed = '', $options = []) { | |
if ($input === null || $input === '') { | |
return ''; | |
} | |
// Convert to string | |
$str = (string)$input; | |
// Default options | |
$defaults = [ | |
'encode_unsafe_chars' => true, | |
'remove_javascript_attributes' => true, | |
'allow_data_attributes' => false, | |
'remove_empty_tags' => true | |
]; | |
$config = array_merge($defaults, $options); | |
// Parse allowed tags | |
$allowedTags = []; | |
if ($allowed) { | |
preg_match_all('/<([a-z][a-z0-9]*)\b[^>]*>/i', $allowed, $matches); | |
if (!empty($matches[1])) { | |
$allowedTags = array_map('strtolower', $matches[1]); | |
} | |
} | |
$allowedTags = array_unique($allowedTags); | |
// Remove comments and PHP tags | |
$result = preg_replace('/<!--[\s\S]*?-->|<\?(?:php)?[\s\S]*?\?>/i', '', $str); | |
// Remove script and style tags completely with their content | |
$result = preg_replace('/<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>/i', '', $result); | |
// Process HTML tags | |
$result = preg_replace_callback('/<\/?([a-z][a-z0-9]*)\b[^>]*>/i', | |
function($matches) use ($allowedTags, $config) { | |
return process_tag($matches, $allowedTags, $config); | |
}, | |
$result | |
); | |
// Additional security measures | |
if ($config['encode_unsafe_chars']) { | |
$result = htmlspecialchars($result, ENT_QUOTES | ENT_HTML5, 'UTF-8', false); | |
// Decode allowed tags back to normal | |
foreach ($allowedTags as $tag) { | |
$openTag = '<' . $tag; | |
$closeTag = '</' . $tag; | |
$result = str_replace([$openTag, $closeTag], ['<' . $tag, '</' . $tag], $result); | |
} | |
} | |
if ($config['remove_empty_tags']) { | |
$result = preg_replace('/<([a-z][a-z0-9]*)([^>]*)\s*>\s*<\/\1>/i', '', $result); | |
} | |
return $result; | |
} | |
function process_tag($matches, $allowedTags, $config) { | |
$fullTag = $matches[0]; | |
$tagName = strtolower($matches[1]); | |
// Check if tag is allowed | |
if (!in_array($tagName, $allowedTags)) { | |
return ''; | |
} | |
// Secure the allowed tag | |
return secure_tag($fullTag, $tagName, $config); | |
} | |
function secure_tag($tag, $tagName, $config) { | |
// List of dangerous attributes | |
$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' | |
]; | |
$dangerousProtocols = ['javascript:', 'vbscript:', 'data:', 'livescript:']; | |
// For closing tags, return as-is | |
if (strpos($tag, '</') === 0) { | |
return $tag; | |
} | |
// Remove dangerous attributes | |
if ($config['remove_javascript_attributes']) { | |
// Remove event handlers | |
foreach ($dangerousAttrs as $attr) { | |
$tag = preg_replace('/\s+' . preg_quote($attr, '/') . '\s*=\s*"[^"]*"/i', '', $tag); | |
$tag = preg_replace('/\s+' . preg_quote($attr, '/') . '\s*=\s*\'[^\']*\'/i', '', $tag); | |
$tag = preg_replace('/\s+' . preg_quote($attr, '/') . '\s*=\s*[^\s>]+/i', '', $tag); | |
} | |
// Remove dangerous protocols from href, src, action | |
$tag = preg_replace_callback('/(href|src|action|background)\s*=\s*["\']([^"\']*)["\']/i', | |
function($matches) use ($dangerousProtocols) { | |
$value = $matches[2]; | |
foreach ($dangerousProtocols as $protocol) { | |
if (stripos($value, $protocol) === 0) { | |
return $matches[1] . '=""'; // Empty the attribute | |
} | |
} | |
return $matches[0]; | |
}, | |
$tag | |
); | |
// Remove style attributes with dangerous expressions | |
$tag = preg_replace_callback('/style\s*=\s*["\']([^"\']*)["\']/i', | |
function($matches) { | |
if (preg_match('/(expression|javascript|vbscript|url\s*\([^)]*javascript:)/i', $matches[1])) { | |
return 'style=""'; | |
} | |
return $matches[0]; | |
}, | |
$tag | |
); | |
// Remove data attributes if not allowed | |
if (!$config['allow_data_attributes']) { | |
$tag = preg_replace('/\s+data-\w+\s*=\s*"[^"]*"/i', '', $tag); | |
$tag = preg_replace('/\s+data-\w+\s*=\s*\'[^\']*\'/i', '', $tag); | |
$tag = preg_replace('/\s+data-\w+\s*=\s*[^\s>]+/i', '', $tag); | |
} | |
} | |
return $tag; | |
} | |
// Basic version (similar to original JavaScript function) | |
function strip_tags_basic($input, $allowed = '') { | |
if ($input === null || $input === '') { | |
return ''; | |
} | |
$str = (string)$input; | |
// Parse allowed tags | |
$allowedTags = []; | |
if ($allowed) { | |
preg_match_all('/<([a-z][a-z0-9]*)\b[^>]*>/i', $allowed, $matches); | |
if (!empty($matches[1])) { | |
$allowedTags = array_map('strtolower', $matches[1]); | |
} | |
} | |
// Remove comments and PHP tags | |
$result = preg_replace('/<!--[\s\S]*?-->|<\?(?:php)?[\s\S]*?\?>/i', '', $str); | |
// Remove script and style tags | |
$result = preg_replace('/<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>/i', '', $result); | |
// Process remaining tags | |
$result = preg_replace_callback('/<\/?([a-z][a-z0-9]*)\b[^>]*>/i', | |
function($matches) use ($allowedTags) { | |
$tagName = strtolower($matches[1]); | |
if (in_array($tagName, $allowedTags)) { | |
return $matches[0]; | |
} | |
return ''; | |
}, | |
$result | |
); | |
return $result; | |
} | |
// Usage examples: | |
$html = '<p onclick="alert(\'xss\')">Hello <b>world</b>! <script>alert("xss")</script></p>'; | |
echo "Basic version:\n"; | |
echo strip_tags_basic($html, '<p><b>') . "\n\n"; | |
echo "Secure version:\n"; | |
echo strip_tags_secure($html, '<p><b>', [ | |
'remove_javascript_attributes' => true, | |
'encode_unsafe_chars' => true | |
]) . "\n\n"; | |
// Test against XSS attacks | |
$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>', | |
'<div onmouseover="alert(1)">Hover</div>' | |
]; | |
echo "XSS Protection Tests:\n"; | |
foreach ($xssTests as $test) { | |
$secured = strip_tags_secure($test, '<p><img><a><div>', [ | |
'remove_javascript_attributes' => true | |
]); | |
echo "Input: " . htmlspecialchars($test) . "\n"; | |
echo "Output: " . htmlspecialchars($secured) . "\n\n"; | |
} | |
?> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.