diff options
author | 2022-12-16 14:14:42 -0500 | |
---|---|---|
committer | 2022-12-16 13:14:42 -0600 | |
commit | d85ec7484ce14a4c7d3f480da8f38fcb9aff388f (patch) | |
tree | dff6dc0617f3cc345b7ece9553e3e7f8d73793c2 | |
parent | d1abb63a648ec1d95b504d9c89cdfc7217d36b08 (diff) | |
download | astro-d85ec7484ce14a4c7d3f480da8f38fcb9aff388f.tar.gz astro-d85ec7484ce14a4c7d3f480da8f38fcb9aff388f.tar.zst astro-d85ec7484ce14a4c7d3f480da8f38fcb9aff388f.zip |
Sanitize dynamic tags (#5615)
* fix: sanitize tags
* fix: better element sanitization
* chore: remove unused import
Co-authored-by: Nate Moore <nate@astro.build>
-rw-r--r-- | .changeset/fresh-bats-prove.md | 5 | ||||
-rw-r--r-- | packages/astro/src/runtime/server/render/component.ts | 14 | ||||
-rw-r--r-- | packages/astro/test/units/render/components.test.js | 65 |
3 files changed, 81 insertions, 3 deletions
diff --git a/.changeset/fresh-bats-prove.md b/.changeset/fresh-bats-prove.md new file mode 100644 index 000000000..4176b0d82 --- /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 d79992a51..69fcfa160 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}</${Component}>` + : `>${childSlots}</${Tag}>` )}` ); 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 000000000..6b13c2562 --- /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><script id="pwnd">console.log("pwnd")</script>' + --- + <html> + <head><title>testing</title></head> + <body> + <TagA id="target" /> + <TagB /> + </body> + </html> + `, + }, + 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); + } + ); + }); +}); |