From 85211b927138d64ef78a031043af29af4657aec1 Mon Sep 17 00:00:00 2001 From: Chris Zuber Date: Mon, 5 Jan 2026 14:00:33 -0800 Subject: [PATCH] Add Trusted Types policy and html template tag Introduces a new `createPolicy()` function in `trusted-html.js` to create a Trusted Types policy for HTML sanitization, supporting both direct string and tagged template usage. Adds an `html` tagged template function to automatically escape interpolated values, updates exports, configuration, and tests, and bumps the package version to 1.0.2. --- CHANGELOG.md | 5 +++++ html.js | 7 +++++++ html.test.js | 22 ++++++++++++++++++- http.config.js | 2 +- index.js | 1 + package-lock.json | 4 ++-- package.json | 6 +++++- rollup.config.js | 11 ++++++++++ trusted-html.js | 37 ++++++++++++++++++++++++++++++++ trusted-html.test.js | 50 ++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 140 insertions(+), 5 deletions(-) create mode 100644 trusted-html.js create mode 100644 trusted-html.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index f37f425..32ca651 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.2] - 2026-01-05 + +### Added +- Add `createPolicy()` to create a `TrustedTypePolicy` with a `createHTML()` that escapes untrusted input + ## [v1.0.1] - 2026-01-05 ### Added diff --git a/html.js b/html.js index c73ecc1..9068dac 100644 --- a/html.js +++ b/html.js @@ -22,6 +22,13 @@ const _str = (str, fallback = '') => (str?.toString?.() ?? fallback); export const escapeHTML = str => _str(str) .replaceAll(HTML_UNSAFE_PATTERN, char => HTML_REPLACEMENTS[char]); +/** + * Tagged template function that automatically escapes interpolated values. + * @example html`
${content}
` + */ +export function html(strings, ...values) { + return String.raw(strings, ...values.map(escapeHTML)); +} /** * Escapes characters that are unsafe or prohibited in HTML attribute names. diff --git a/html.test.js b/html.test.js index bfda068..6b95ef1 100644 --- a/html.test.js +++ b/html.test.js @@ -1,6 +1,6 @@ import assert from 'node:assert'; import { describe, test, before } from 'node:test'; -import { escapeHTML, escapeAttrName, stringifyAttr } from './html.js'; +import { escapeHTML, escapeAttrName, stringifyAttr, html } from './html.js'; // Mock DOM Attr class for Node environment class MockAttr { @@ -114,3 +114,23 @@ describe('Security Escaping Utils', () => { }); }); }); + +describe('html tagged template', () => { + test('escapes interpolated values', () => { + const malicious = ''); + assert.strictEqual(result, '<script>alert(1)</script>'); + }); + + test('Array Usage: joins and escapes list items', () => { + // Essential to ensure arrays are not stringified with commas + const items = ['
', 'bold']; + const result = html`Items: ${items}`; + + assert.strictEqual(result, 'Items: <br>,<b>bold</b>'); + }); + + test('Security: enforces Double Escaping in fallback mode', () => { + /* * CRITICAL SECURITY CHECK: + * Since we are in Node (no TrustedTypes), the output of `html` is just a string. + * If we nest this string into another template, it MUST be escaped again. + * If it wasn't, we would be vulnerable to "fake trust". + */ + const inner = html`Safe`; + // inner is string: "Safe" + + const outer = html`
${inner}
`; + // outer sees a string, so it escapes it: "
<span>Safe</span>
" + + assert.strictEqual(outer, '
<span>Safe</span>
'); + }); + + test('Interface: returns a policy-like object', () => { + // Ensures the return value matches the TrustedTypePolicy interface shape + assert.strictEqual(typeof policy.createHTML, 'function'); + assert.strictEqual(policy.name, 'default'); + }); +});