diff --git a/CHANGELOG.md b/CHANGELOG.md index 32ca651..8f843d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [v1.0.3] - 2026-01-05 + +### Fixed +- Fix bad logic in the `trustedTypes` / tagged template / `trusted-html.js` implementation + ## [v1.0.2] - 2026-01-05 ### Added diff --git a/html.js b/html.js index 9068dac..6d3218a 100644 --- a/html.js +++ b/html.js @@ -1,4 +1,4 @@ -export const HTML_UNSAFE_PATTERN = /[<>"']|&(?![a-zA-Z\d]{2,5};|#\d{1,3};)/g; +export const HTML_UNSAFE_PATTERN = /[<>"']|&(?![a-zA-Z\d]{2,40};|#\d{1,6};)/g; /* eslint no-control-regex: "off" */ export const ATTR_NAME_UNSAFE_PATTERN = /[\u0000-\u001f\u007f-\u009f\s"'\\/=><&]/g; export const HTML_REPLACEMENTS = Object.freeze({ diff --git a/http.config.js b/http.config.js index c641d52..f6b25c1 100644 --- a/http.config.js +++ b/http.config.js @@ -7,7 +7,7 @@ addScriptSrc( 'https://unpkg.com/@shgysk8zer0/', ); -addTrustedTypePolicy('aegis-sanitizer#html', 'default'); +addTrustedTypePolicy('aegis-sanitizer#html', 'aegis-escape#html'); export default { routes: { diff --git a/index.js b/index.js index 0a145cd..5ef73dd 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,3 @@ -export { HTML_UNSAFE_PATTERN, ATTR_NAME_UNSAFE_PATTERN, HTML_REPLACEMENTS, escapeHTML, escapeAttrName, stringifyAttr } from './html.js'; -export { createPolicy } from './trusted-html.js'; +export { HTML_UNSAFE_PATTERN, ATTR_NAME_UNSAFE_PATTERN, HTML_REPLACEMENTS, escapeHTML, escapeAttrName, stringifyAttr, html } from './html.js'; +export { html as trustedHTML } from './trusted-html.js'; export { escapeCSS } from './css.js'; diff --git a/package-lock.json b/package-lock.json index 828ed5f..88c9e4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@aegisjsproject/escape", - "version": "1.0.2", + "version": "1.0.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@aegisjsproject/escape", - "version": "1.0.2", + "version": "1.0.3", "funding": [ { "type": "librepay", diff --git a/package.json b/package.json index ed31340..9a59166 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aegisjsproject/escape", - "version": "1.0.2", + "version": "1.0.3", "description": "String escaping utilities for HTML and DOM attributes.", "keywords": [ "security", diff --git a/trusted-html.js b/trusted-html.js index 6aa06fd..69abf55 100644 --- a/trusted-html.js +++ b/trusted-html.js @@ -1,37 +1,40 @@ import { escapeHTML } from './html.js'; const SUPPORTS_TRUSTED_TYPES = 'trustedTypes' in globalThis; -const isTrustedHTML = SUPPORTS_TRUSTED_TYPES ? globalThis.trustedTypes.isHTML : () => false; +const POLICY_NAME = 'aegis-escape#html'; +const TRUSTED_SYMBOL = Symbol(POLICY_NAME); -function createHTML(strings, ...values) { +const isTrustedHTML = SUPPORTS_TRUSTED_TYPES + ? input => globalThis.trustedTypes.isHTML(input) + : input => typeof input === 'object' ? Object.hasOwn(input ?? {}, TRUSTED_SYMBOL) : false; + +const policy = SUPPORTS_TRUSTED_TYPES + ? globalThis.trustedTypes.createPolicy(POLICY_NAME, { createHTML: input => input }) + : Object.freeze({ + name: POLICY_NAME, + createHTML(input) { + const obj = { + toString() { + return input; + } + }; + + Object.defineProperty(obj, TRUSTED_SYMBOL, { + value: true, + enumerable: false, + writable: false, + }); + + return Object.freeze(obj); + } + }); + +export function html(strings, ...values) { if (! Array.isArray(strings) || ! Array.isArray(strings.raw)) { - return Array.isArray(strings) - ? strings.map(escapeHTML).join('') - : escapeHTML(strings); + return policy.createHTML(Array.isArray(strings) + ? strings.map(input => isTrustedHTML(input) ? input : escapeHTML(input)).join('') + : escapeHTML(strings)); } else { - return String.raw(strings, ...values.map(val => isTrustedHTML(val) ? val : escapeHTML(val))); + return policy.createHTML(String.raw(strings, ...values.map(val => isTrustedHTML(val) ? val : escapeHTML(val)))); } } - -/** - * Creates a Trusted Types policy (or a compliant fallback) for sanitizing HTML. - * The returned policy's `createHTML` method is overloaded to handle both direct string arguments - * and tagged template literals. - * - * @param {string} [name="aegis-escape#html"] - The policy name. Must match your CSP `trusted-types` directive. - * @returns {TrustedTypePolicy} A native Policy object or a frozen fallback matching the interface. - * - * @example - * const policy = createPolicy(); - * - * // 1. As a regular method: - * policy.createHTML('