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:
root
2025-12-25 23:39:42 +00:00
parent 1322bbf988
commit 1abbac58e7
419 changed files with 39662 additions and 154 deletions

View File

@@ -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'",
],
];
}
}

View File

@@ -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

View File

@@ -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
*

View 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)