utilikit-1.0.0/js/utilikit.security.js
js/utilikit.security.js
/**
* @file
* UtiliKit Security Framework and XSS Prevention System.
*
* This file provides comprehensive security utilities and sanitization
* functions to protect against XSS attacks and ensure safe handling of
* user-generated content within the UtiliKit system. It implements multiple
* layers of protection including input validation, output sanitization,
* and secure DOM manipulation.
*
* Security Features:
* - Text content sanitization to prevent script injection
* - HTML sanitization with allowlist-based tag filtering
* - CSS class name validation and escaping
* - Server response sanitization
* - Safe DOM element creation utilities
* - Security patches for existing UtiliKit functions
*
* The security system follows defense-in-depth principles, validating
* inputs at multiple stages and providing secure alternatives to
* potentially dangerous operations.
*
* @see utilikit.behavior.js for secure element processing
* @see utilikit.reference.js for secure copy functionality
*/
(function(Drupal, once, drupalSettings) {
'use strict';
Drupal.utilikit = Drupal.utilikit || {};
/**
* UtiliKit Security Utilities Namespace.
*
* Provides a collection of security-focused functions for safe handling
* of user input, DOM manipulation, and content sanitization within the
* UtiliKit ecosystem.
*
* @namespace
*/
Drupal.utilikit.security = {
/**
* Sanitizes text content to prevent XSS attacks.
*
* Uses the browser's built-in HTML parsing to safely encode any
* potentially dangerous characters. This function converts all HTML
* entities and special characters to their safe encoded equivalents.
*
* @param {string} text
* The text content to sanitize.
*
* @returns {string}
* The sanitized text with HTML entities encoded.
*
* @example
* // Dangerous input
* const userInput = '<script>alert("xss")</script>';
* // Safe output
* const safe = Drupal.utilikit.security.sanitizeText(userInput);
* // Result: '<script>alert("xss")</script>'
*/
sanitizeText: function(text) {
if (typeof text !== 'string') {
return '';
}
// Create a temporary element to leverage browser's HTML parsing for safety
const temp = document.createElement('div');
temp.textContent = text;
return temp.innerHTML;
},
/**
* Sanitizes HTML content using allowlist-based tag filtering.
*
* Processes HTML content to remove any potentially dangerous elements
* while preserving allowed tags. All attributes are stripped from
* allowed elements to prevent attribute-based attacks. Non-allowed
* elements are replaced with their text content.
*
* @param {string} html
* The HTML content to sanitize.
* @param {Array<string>} allowedTags
* Array of allowed HTML tag names (lowercase). Defaults to empty array.
*
* @returns {string}
* The sanitized HTML with only allowed tags preserved.
*
* @example
* const dirtyHtml = '<p>Safe text</p><script>alert("bad")</script>';
* const cleanHtml = Drupal.utilikit.security.sanitizeHtml(
* dirtyHtml,
* ['p', 'strong', 'em']
* );
* // Result: '<p>Safe text</p>alert("bad")'
*/
sanitizeHtml: function(html, allowedTags = []) {
if (typeof html !== 'string') {
return '';
}
// If no tags allowed, just return text content
if (allowedTags.length === 0) {
return this.sanitizeText(html);
}
// Create temporary DOM to parse and clean
const temp = document.createElement('div');
temp.innerHTML = html;
// Remove all non-allowed elements
const allElements = temp.querySelectorAll('*');
allElements.forEach(element => {
if (!allowedTags.includes(element.tagName.toLowerCase())) {
// Replace with text content
const textNode = document.createTextNode(element.textContent);
element.parentNode.replaceChild(textNode, element);
} else {
// Remove all attributes from allowed elements for safety
while (element.attributes.length > 0) {
element.removeAttribute(element.attributes[0].name);
}
}
});
return temp.innerHTML;
},
/**
* Validates and sanitizes CSS class names for security.
*
* Ensures class names contain only safe characters and are within
* reasonable length limits. Prevents CSS injection attacks and
* malformed class names that could break styling or cause errors.
*
* @param {string} className
* The CSS class name to validate.
*
* @returns {string|null}
* The validated class name if safe, null if invalid or dangerous.
*
* @example
* const safeName = Drupal.utilikit.security.validateClassName('my-class');
* // Result: 'my-class'
*
* const dangerousName = Drupal.utilikit.security.validateClassName('class;color:red');
* // Result: null
*/
validateClassName: function(className) {
if (typeof className !== 'string') {
return null;
}
// Only allow alphanumeric, hyphens, underscores
const validPattern = /^[a-zA-Z0-9\-_]+$/;
if (!validPattern.test(className)) {
return null;
}
// Must be reasonable length
if (className.length > 200) {
return null;
}
return className;
},
/**
* Escapes CSS selectors to prevent CSS injection attacks.
*
* Uses the browser's CSS.escape() method when available, with a
* fallback implementation for older browsers. Properly escapes
* special characters that could be used for CSS injection.
*
* @param {string} selector
* The CSS selector string to escape.
*
* @returns {string}
* The escaped CSS selector safe for use in stylesheets.
*
* @example
* const selector = Drupal.utilikit.security.escapeCssSelector('my:class');
* // Result: 'my\\:class'
*/
escapeCssSelector: function(selector) {
if (typeof selector !== 'string') {
return '';
}
// CSS.escape() is the standard way, with fallback
if (typeof CSS !== 'undefined' && CSS.escape) {
return CSS.escape(selector);
}
// Fallback manual escaping for older browsers
return selector.replace(/[!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g, '\\$&');
},
/**
* Validates UtiliKit utility class name format.
*
* Checks if a class name follows the correct UtiliKit naming convention
* and contains only safe characters. Validates the prefix structure,
* responsive breakpoints, and value patterns used by UtiliKit.
*
* @param {string} className
* The class name to validate against UtiliKit patterns.
*
* @returns {boolean}
* TRUE if the class follows valid UtiliKit format, FALSE otherwise.
*
* @example
* const isValid = Drupal.utilikit.security.isValidUtilityClass('uk-pd--16');
* // Result: true
*
* const isInvalid = Drupal.utilikit.security.isValidUtilityClass('malicious-class');
* // Result: false
*/
isValidUtilityClass: function(className) {
if (typeof className !== 'string') {
return false;
}
// Must start with uk-
if (!className.startsWith('uk-')) {
return false;
}
// Validate against the pattern
const pattern = /^uk-(?:(?:sm|md|lg|xl|xxl)-)?[a-z]{2,4}--[a-zA-Z0-9\-_.%]+$/;
return pattern.test(className);
},
/**
* Sanitizes server response objects for safe consumption.
*
* Processes AJAX and API responses to ensure they contain only
* safe, expected data types. Sanitizes text fields and validates
* numeric values to prevent injection attacks through server responses.
*
* @param {*} response
* The server response object to sanitize.
*
* @returns {Object}
* A sanitized response object with validated fields.
*
* @example
* const serverResponse = {
* message: '<script>alert("xss")</script>',
* count: '42',
* malicious: 'ignored'
* };
* const safe = Drupal.utilikit.security.sanitizeServerResponse(serverResponse);
* // Result: { message: '<script>...', count: 42 }
*/
sanitizeServerResponse: function(response) {
const sanitized = {};
if (response && typeof response === 'object') {
// Sanitize common response fields
if (response.message) {
sanitized.message = this.sanitizeText(response.message);
}
if (response.status) {
sanitized.status = this.sanitizeText(response.status);
}
if (response.count !== undefined) {
sanitized.count = parseInt(response.count) || 0;
}
if (response.timestamp !== undefined) {
sanitized.timestamp = parseInt(response.timestamp) || 0;
}
}
return sanitized;
},
/**
* Creates safe DOM elements with validated content and attributes.
*
* Provides a secure alternative to createElement by validating tag names,
* sanitizing text content, and filtering attributes to prevent XSS
* attacks. Only allows safe HTML tags and attributes.
*
* @param {string} tagName
* The HTML tag name for the element (must be in allowlist).
* @param {string} textContent
* Text content for the element (will be sanitized).
* @param {Object} attributes
* Object of attributes to set (will be filtered and validated).
*
* @returns {Element}
* A safe DOM element with sanitized content and attributes.
*
* @example
* const safeBtn = Drupal.utilikit.security.createSafeElement(
* 'button',
* 'Click me',
* { class: 'btn-primary', id: 'safe-button' }
* );
*/
createSafeElement: function(tagName, textContent = '', attributes = {}) {
// Validate tag name
const allowedTags = ['div', 'span', 'p', 'button', 'strong', 'em', 'code', 'pre'];
if (!allowedTags.includes(tagName.toLowerCase())) {
tagName = 'div'; // Default to safe tag
}
const element = document.createElement(tagName);
// Set text content safely
if (textContent) {
element.textContent = textContent;
}
// Set safe attributes
const allowedAttributes = ['class', 'id', 'title', 'aria-label', 'aria-describedby', 'role'];
Object.keys(attributes).forEach(attr => {
if (allowedAttributes.includes(attr.toLowerCase())) {
const value = this.sanitizeText(attributes[attr]);
if (value) {
element.setAttribute(attr, value);
}
}
});
return element;
}
};
/**
* Safely sets HTML content using allowlist-based sanitization.
*
* Provides a secure alternative to innerHTML by sanitizing content
* before insertion. Uses the allowlist approach to preserve only
* explicitly permitted HTML tags while removing potentially dangerous
* elements and attributes.
*
* @param {Element} element
* The DOM element to set content on.
* @param {string} html
* The HTML content to set (will be sanitized).
* @param {Array<string>} allowedTags
* Array of HTML tag names to preserve. Defaults to empty array.
*
* @example
* const container = document.getElementById('content');
* Drupal.utilikit.safeSetHtml(
* container,
* '<p>Safe content</p><script>dangerous()</script>',
* ['p', 'strong', 'em']
* );
* // Only the <p> tag will be preserved
*/
Drupal.utilikit.safeSetHtml = function(element, html, allowedTags = []) {
if (!element || !element.nodeType) {
return;
}
const sanitized = Drupal.utilikit.security.sanitizeHtml(html, allowedTags);
element.innerHTML = sanitized;
};
/**
* Displays safe user messages with XSS protection.
*
* Creates and displays user notification messages using secure DOM
* manipulation methods. Automatically removes messages after a timeout
* and ensures all content is properly sanitized.
*
* @param {string} text
* The message text to display (will be sanitized).
* @param {string} type
* The message type for styling ('info', 'success', 'warning', 'error').
* @param {string} iconHtml
* Optional icon HTML content (will be sanitized).
*
* @example
* Drupal.utilikit.showSafeMessage(
* 'Settings saved successfully!',
* 'success',
* '<span class="icon-check"></span>'
* );
*/
Drupal.utilikit.showSafeMessage = function(text, type = 'info', iconHtml = '') {
const messageContainer = Drupal.utilikit.security.createSafeElement('div', '', {
'class': `utilikit-update-message utilikit-update-message--${type}`,
'role': 'alert',
'aria-live': 'polite'
});
// Create icon element safely
if (iconHtml) {
const iconElement = Drupal.utilikit.security.createSafeElement('span', iconHtml, {
'class': 'message-icon',
'aria-hidden': 'true'
});
messageContainer.appendChild(iconElement);
}
// Create text element safely
const textElement = Drupal.utilikit.security.createSafeElement('span', text, {
'class': 'message-text'
});
messageContainer.appendChild(textElement);
document.body.appendChild(messageContainer);
// Auto-remove with cleanup
setTimeout(() => {
if (messageContainer.parentNode) {
messageContainer.parentNode.removeChild(messageContainer);
}
}, 3000);
};
})(Drupal, once, drupalSettings);
/**
* @file
* Security Patches for Existing UtiliKit Functions.
*
* This section applies security patches to existing UtiliKit behaviors
* and functions to retrofit them with XSS protection and input validation.
* These patches wrap existing functionality with security checks without
* breaking the original API.
*/
(function(Drupal, once, drupalSettings) {
'use strict';
/**
* Security patch for UtiliKit Reference page behavior.
*
* Wraps the reference page attach function to add security validation
* for any unsafe HTML content. Processes elements marked with
* data-unsafe-html attributes and sanitizes them using allowlisted tags.
*/
if (Drupal.behaviors.utilikitReference) {
const originalAttach = Drupal.behaviors.utilikitReference.attach;
Drupal.behaviors.utilikitReference.attach = function(context, settings) {
// Call original but with security patches
originalAttach.call(this, context, settings);
// Patch any unsafe innerHTML usage
context.querySelectorAll('.utilikit-reference-page [data-unsafe-html]').forEach(element => {
const unsafeHtml = element.getAttribute('data-unsafe-html');
element.removeAttribute('data-unsafe-html');
Drupal.utilikit.safeSetHtml(element, unsafeHtml, ['strong', 'em', 'code']);
});
};
}
/**
* Security patch for UtiliKit class application system.
*
* Wraps the applyClasses function to validate all elements and their
* CSS classes before processing. Filters out elements with invalid
* or potentially dangerous class names to prevent CSS injection attacks.
*/
if (Drupal.utilikit && Drupal.utilikit.applyClasses) {
const originalApplyClasses = Drupal.utilikit.applyClasses;
Drupal.utilikit.applyClasses = function(elements) {
try {
// Validate all elements and classes before processing
const safeElements = Array.from(elements).filter(el => {
if (!el || !el.classList) return false;
// Validate all classes on this element
const classes = Array.from(el.classList);
return classes.every(className => {
return Drupal.utilikit.security.isValidUtilityClass(className) ||
className === 'utilikit' ||
Drupal.utilikit.security.validateClassName(className);
});
});
if (safeElements.length > 0) {
originalApplyClasses.call(this, safeElements);
}
if (safeElements.length < elements.length) {
console.warn('UtiliKit: Some elements filtered out for security reasons');
}
} catch (error) {
console.error('UtiliKit: Security error in applyClasses:', error);
// Fail securely - don't apply any classes
}
};
}
/**
* Security notification for update button functionality.
*
* Logs a warning if the updateUtilikit function exists but hasn't been
* properly patched with security measures. This serves as a reminder
* to apply security patches to the update button implementation.
*/
if (typeof updateUtilikit === 'function') {
// This would need to be applied to the update button code
console.warn('UtiliKit: Update button needs security patching');
}
})(Drupal, once, drupalSettings);
