11import assert from 'node:assert/strict' ;
2- import { describe , it } from 'node:test' ;
2+ import { after , before , describe , it } from 'node:test' ;
33import * as cheerio from 'cheerio' ;
44import {
55 addAttribute ,
@@ -15,6 +15,7 @@ import {
1515 Fragment ,
1616 render as renderTemplate ,
1717 renderComponent ,
18+ renderHTMLElement ,
1819 renderSlot ,
1920 unescapeHTML ,
2021} from '../../../dist/runtime/server/index.js' ;
@@ -263,6 +264,54 @@ describe('spreadAttributes rejects invalid attribute keys', () => {
263264 } ) ;
264265} ) ;
265266
267+ describe ( 'renderHTMLElement rejects invalid attribute keys' , ( ) => {
268+ // renderHTMLElement resolves the tag name through customElements.getName().
269+ // In a Node test environment this global doesn't exist, so we stub it.
270+ const originalCustomElements = globalThis . customElements ;
271+ const result = { } as any ;
272+
273+ before ( ( ) => {
274+ globalThis . customElements = {
275+ getName : ( ) => 'my-el' ,
276+ } as any ;
277+ } ) ;
278+
279+ after ( ( ) => {
280+ globalThis . customElements = originalCustomElements ;
281+ } ) ;
282+
283+ it ( 'drops malicious keys while keeping valid ones' , async ( ) => {
284+ const html = await renderHTMLElement (
285+ result ,
286+ class { } as any ,
287+ {
288+ 'onmouseover=alert(document.domain) x' : 'y' ,
289+ 'x><script>alert(1)</script>' : 'z' ,
290+ 'data-safe' : 'ok' ,
291+ } ,
292+ { } ,
293+ ) ;
294+ const output = String ( html ) ;
295+ assert . ok ( output . includes ( 'data-safe="ok"' ) ) ;
296+ assert . ok ( ! output . includes ( 'onmouseover' ) ) ;
297+ assert . ok ( ! output . includes ( '<script>' ) ) ;
298+ assert . ok ( ! output . includes ( 'alert' ) ) ;
299+ } ) ;
300+
301+ it ( 'preserves namespaced and normal attribute names' , async ( ) => {
302+ const html = await renderHTMLElement (
303+ result ,
304+ class { } as any ,
305+ { 'id' : 'a' , 'data-foo' : 'b' , 'on:click' : 'c' } ,
306+ { } ,
307+ ) ;
308+ const output = String ( html ) ;
309+ assert . ok ( output . includes ( 'id="a"' ) ) ;
310+ assert . ok ( output . includes ( 'data-foo="b"' ) ) ;
311+ assert . ok ( output . includes ( 'on:click="c"' ) ) ;
312+ } ) ;
313+ } ) ;
314+
266315describe ( 'Correctly serializes boolean attributes (#astro-basic)' , async ( ) => {
267316 // h1 data-something and h2 not-data-ok are both empty-string boolean-ish attrs
268317 it ( 'renders empty-value attribute for data-* attr with no value' , ( ) => {
0 commit comments