Add safe_html() for XSS-safe WYSIWYG HTML sanitization
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,9 @@ class Core_Bundle extends Rsx_Bundle_Abstract
|
||||
'app/RSpade/Breadcrumbs', // Progressive breadcrumb resolution
|
||||
'app/RSpade/Lib',
|
||||
],
|
||||
'npm' => [
|
||||
'DOMPurify' => "import DOMPurify from 'dompurify'",
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,6 +285,23 @@ function html(str) {
|
||||
return _.escape(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes HTML from WYSIWYG editors to prevent XSS attacks
|
||||
*
|
||||
* Uses DOMPurify to filter potentially malicious HTML while preserving
|
||||
* safe formatting tags. Suitable for user-generated rich text content.
|
||||
*
|
||||
* @param {string} html_string - HTML string to sanitize
|
||||
* @returns {string} Sanitized HTML safe for display
|
||||
*/
|
||||
function safe_html(html_string) {
|
||||
return DOMPurify.sanitize(html_string, {
|
||||
ALLOWED_TAGS: ['p', 'br', 'strong', 'b', 'em', 'i', 'u', 's', 'strike', 'a', 'ul', 'ol', 'li', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre', 'code', 'img', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'div', 'span'],
|
||||
ALLOWED_ATTR: ['href', 'title', 'target', 'src', 'alt', 'width', 'height', 'class'],
|
||||
ALLOW_DATA_ATTR: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts newlines to HTML line breaks
|
||||
* @param {string} str - String to convert
|
||||
|
||||
@@ -1182,6 +1182,48 @@ function is_loopback_ip(): bool
|
||||
return in_array($ip, $loopback_addresses, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize HTML from WYSIWYG editors to prevent XSS attacks
|
||||
*
|
||||
* Uses HTMLPurifier to filter potentially malicious HTML while preserving
|
||||
* safe formatting tags. Suitable for user-generated rich text content.
|
||||
*
|
||||
* @param string $html The HTML string to sanitize
|
||||
* @return string Sanitized HTML safe for display
|
||||
*/
|
||||
function safe_html(string $html): string
|
||||
{
|
||||
static $purifier = null;
|
||||
|
||||
if ($purifier === null) {
|
||||
require_once base_path('vendor/ezyang/htmlpurifier/library/HTMLPurifier.auto.php');
|
||||
|
||||
$config = HTMLPurifier_Config::createDefault();
|
||||
|
||||
// Cache serialized definitions for performance
|
||||
$cache_dir = storage_path('rsx-tmp/htmlpurifier');
|
||||
if (!is_dir($cache_dir)) {
|
||||
mkdir($cache_dir, 0755, true);
|
||||
}
|
||||
$config->set('Cache.SerializerPath', $cache_dir);
|
||||
$config->set('Cache.SerializerPermissions', null); // Disable chmod (Docker compatibility)
|
||||
|
||||
// Allow common formatting elements
|
||||
$config->set('HTML.Allowed', 'p,br,strong,b,em,i,u,s,strike,a[href|title|target],ul,ol,li,blockquote,h1,h2,h3,h4,h5,h6,pre,code,img[src|alt|title|width|height],table,thead,tbody,tr,th,td,div,span');
|
||||
|
||||
// Allow class attributes for styling
|
||||
$config->set('Attr.AllowedClasses', null); // Allow all classes
|
||||
|
||||
// Link handling
|
||||
$config->set('HTML.TargetBlank', true);
|
||||
$config->set('URI.AllowedSchemes', ['http' => true, 'https' => true, 'mailto' => true]);
|
||||
|
||||
$purifier = new HTMLPurifier($config);
|
||||
}
|
||||
|
||||
return $purifier->purify($html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a hash for a file suitable for build/cache invalidation
|
||||
*
|
||||
|
||||
66
app/RSpade/man/safe_html.txt
Normal file
66
app/RSpade/man/safe_html.txt
Normal file
@@ -0,0 +1,66 @@
|
||||
SAFE_HTML(1) RSpade Manual SAFE_HTML(1)
|
||||
|
||||
NAME
|
||||
safe_html - Sanitize HTML from WYSIWYG editors to prevent XSS attacks
|
||||
|
||||
SYNOPSIS
|
||||
PHP: safe_html(string $html): string
|
||||
JS: safe_html(html_string)
|
||||
|
||||
DESCRIPTION
|
||||
Filters potentially malicious HTML while preserving safe formatting tags.
|
||||
Use for all user-generated rich text content before display.
|
||||
|
||||
Both PHP (HTMLPurifier) and JS (DOMPurify) implementations use matching
|
||||
allowed tags and attributes for consistent behavior.
|
||||
|
||||
WHAT GETS STRIPPED
|
||||
- <script> tags and contents
|
||||
- Event handlers (onclick, onerror, onload, etc.)
|
||||
- javascript: and data: URLs
|
||||
- <iframe>, <object>, <embed> tags
|
||||
- <style> tags and style attributes with expressions
|
||||
- Any tag/attribute not in the allowed list
|
||||
|
||||
ALLOWED TAGS
|
||||
p, br, strong, b, em, i, u, s, strike, a, ul, ol, li, blockquote,
|
||||
h1, h2, h3, h4, h5, h6, pre, code, img, table, thead, tbody, tr, th, td,
|
||||
div, span
|
||||
|
||||
ALLOWED ATTRIBUTES
|
||||
href, title, target (on links)
|
||||
src, alt, width, height (on images)
|
||||
class (on all elements)
|
||||
|
||||
EXAMPLES
|
||||
PHP:
|
||||
$clean = safe_html($user_input);
|
||||
echo $clean; // Safe to output
|
||||
|
||||
JS:
|
||||
const clean = safe_html(editor.getHTML());
|
||||
container.innerHTML = clean; // Safe to insert
|
||||
|
||||
Input: <p>Hello <script>alert(1)</script></p>
|
||||
Output: <p>Hello </p>
|
||||
|
||||
Input: <a href="javascript:alert(1)">click</a>
|
||||
Output: <a>click</a>
|
||||
|
||||
Input: <img src="x" onerror="alert(1)">
|
||||
Output: <img src="x">
|
||||
|
||||
USAGE PATTERN
|
||||
Always sanitize on the server before storing OR before display.
|
||||
Sanitizing on both client and server provides defense in depth.
|
||||
|
||||
// Controller - sanitize before saving
|
||||
$model->description = safe_html($params['description']);
|
||||
|
||||
// Or sanitize on display in template
|
||||
<%!= safe_html(this.data.description) %>
|
||||
|
||||
SEE ALSO
|
||||
html() - Escape all HTML (for plain text, not rich text)
|
||||
|
||||
RSpade Framework December 2025 SAFE_HTML(1)
|
||||
Reference in New Issue
Block a user