diff --git a/.changeset/fresh-bats-prove.md b/.changeset/fresh-bats-prove.md new file mode 100644 index 000000000000..4176b0d82f2b --- /dev/null +++ b/.changeset/fresh-bats-prove.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Sanitize dynamically rendered tags to strip out any attributes diff --git a/packages/astro/src/runtime/server/render/component.ts b/packages/astro/src/runtime/server/render/component.ts index d79992a51cd0..69fcfa160624 100644 --- a/packages/astro/src/runtime/server/render/component.ts +++ b/packages/astro/src/runtime/server/render/component.ts @@ -239,12 +239,14 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr // This is a custom element without a renderer. Because of that, render it // as a string and the user is responsible for adding a script tag for the component definition. if (!html && typeof Component === 'string') { + // Sanitize tag name because some people might try to inject attributes 🙄 + const Tag = sanitizeElementName(Component); const childSlots = Object.values(children).join(''); const iterable = renderAstroTemplateResult( - await renderTemplate`<${Component}${internalSpreadAttributes(props)}${markHTMLString( - childSlots === '' && voidElementNames.test(Component) + await renderTemplate`<${Tag}${internalSpreadAttributes(props)}${markHTMLString( + childSlots === '' && voidElementNames.test(Tag) ? `/>` - : `>${childSlots}` + : `>${childSlots}` )}` ); html = ''; @@ -322,6 +324,12 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr return renderAll(); } +function sanitizeElementName(tag: string) { + const unsafe = /[&<>'"\s]+/g; + if (!unsafe.test(tag)) return tag; + return tag.trim().split(unsafe)[0].trim(); +} + async function renderFragmentComponent(result: SSRResult, slots: any = {}) { const children = await renderSlot(result, slots?.default); if (children == null) { diff --git a/packages/astro/test/units/render/components.test.js b/packages/astro/test/units/render/components.test.js new file mode 100644 index 000000000000..6b13c25623e6 --- /dev/null +++ b/packages/astro/test/units/render/components.test.js @@ -0,0 +1,65 @@ +import { expect } from 'chai'; +import * as cheerio from 'cheerio'; + +import { runInContainer } from '../../../dist/core/dev/index.js'; +import { createFs, createRequestAndResponse } from '../test-utils.js'; +import svelte from '../../../../integrations/svelte/dist/index.js'; +import { defaultLogging } from '../../test-utils.js'; + +const root = new URL('../../fixtures/alias/', import.meta.url); + +describe('core/render components', () => { + it('should sanitize dynamic tags', async () => { + const fs = createFs( + { + '/src/pages/index.astro': ` + --- + const TagA = 'p style=color:red;' + const TagB = 'p>' + --- + + testing + + + + + + `, + }, + root + ); + + await runInContainer( + { + fs, + root, + logging: { + ...defaultLogging, + // Error is expected in this test + level: 'silent', + }, + userConfig: { + integrations: [svelte()], + }, + }, + async (container) => { + const { req, res, done, text } = createRequestAndResponse({ + method: 'GET', + url: '/', + }); + container.handle(req, res); + + await done; + const html = await text(); + const $ = cheerio.load(html); + const target = $('#target'); + + expect(target).not.to.be.undefined; + expect(target.attr('id')).to.equal('target'); + expect(target.attr('style')).to.be.undefined; + + expect($('#pwnd').length).to.equal(0); + } + ); + }); +});