Skip to content

Commit d85ec74

Browse files
authored
Sanitize dynamic tags (#5615)
* fix: sanitize tags * fix: better element sanitization * chore: remove unused import Co-authored-by: Nate Moore <[email protected]>
1 parent d1abb63 commit d85ec74

File tree

3 files changed

+81
-3
lines changed

3 files changed

+81
-3
lines changed

.changeset/fresh-bats-prove.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'astro': patch
3+
---
4+
5+
Sanitize dynamically rendered tags to strip out any attributes

packages/astro/src/runtime/server/render/component.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -239,12 +239,14 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
239239
// This is a custom element without a renderer. Because of that, render it
240240
// as a string and the user is responsible for adding a script tag for the component definition.
241241
if (!html && typeof Component === 'string') {
242+
// Sanitize tag name because some people might try to inject attributes 🙄
243+
const Tag = sanitizeElementName(Component);
242244
const childSlots = Object.values(children).join('');
243245
const iterable = renderAstroTemplateResult(
244-
await renderTemplate`<${Component}${internalSpreadAttributes(props)}${markHTMLString(
245-
childSlots === '' && voidElementNames.test(Component)
246+
await renderTemplate`<${Tag}${internalSpreadAttributes(props)}${markHTMLString(
247+
childSlots === '' && voidElementNames.test(Tag)
246248
? `/>`
247-
: `>${childSlots}</${Component}>`
249+
: `>${childSlots}</${Tag}>`
248250
)}`
249251
);
250252
html = '';
@@ -322,6 +324,12 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
322324
return renderAll();
323325
}
324326

327+
function sanitizeElementName(tag: string) {
328+
const unsafe = /[&<>'"\s]+/g;
329+
if (!unsafe.test(tag)) return tag;
330+
return tag.trim().split(unsafe)[0].trim();
331+
}
332+
325333
async function renderFragmentComponent(result: SSRResult, slots: any = {}) {
326334
const children = await renderSlot(result, slots?.default);
327335
if (children == null) {
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { expect } from 'chai';
2+
import * as cheerio from 'cheerio';
3+
4+
import { runInContainer } from '../../../dist/core/dev/index.js';
5+
import { createFs, createRequestAndResponse } from '../test-utils.js';
6+
import svelte from '../../../../integrations/svelte/dist/index.js';
7+
import { defaultLogging } from '../../test-utils.js';
8+
9+
const root = new URL('../../fixtures/alias/', import.meta.url);
10+
11+
describe('core/render components', () => {
12+
it('should sanitize dynamic tags', async () => {
13+
const fs = createFs(
14+
{
15+
'/src/pages/index.astro': `
16+
---
17+
const TagA = 'p style=color:red;'
18+
const TagB = 'p><script id="pwnd">console.log("pwnd")</script>'
19+
---
20+
<html>
21+
<head><title>testing</title></head>
22+
<body>
23+
<TagA id="target" />
24+
<TagB />
25+
</body>
26+
</html>
27+
`,
28+
},
29+
root
30+
);
31+
32+
await runInContainer(
33+
{
34+
fs,
35+
root,
36+
logging: {
37+
...defaultLogging,
38+
// Error is expected in this test
39+
level: 'silent',
40+
},
41+
userConfig: {
42+
integrations: [svelte()],
43+
},
44+
},
45+
async (container) => {
46+
const { req, res, done, text } = createRequestAndResponse({
47+
method: 'GET',
48+
url: '/',
49+
});
50+
container.handle(req, res);
51+
52+
await done;
53+
const html = await text();
54+
const $ = cheerio.load(html);
55+
const target = $('#target');
56+
57+
expect(target).not.to.be.undefined;
58+
expect(target.attr('id')).to.equal('target');
59+
expect(target.attr('style')).to.be.undefined;
60+
61+
expect($('#pwnd').length).to.equal(0);
62+
}
63+
);
64+
});
65+
});

0 commit comments

Comments
 (0)