Escape HTML by default (#2747)

* feat: escape HTML by default

* feat(test): add escaping test
This commit is contained in:
Nate Moore 2022-03-09 17:02:25 -06:00 committed by GitHub
parent 658a92915d
commit 05b66bd68b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 35 additions and 26 deletions

View file

@ -0,0 +1,5 @@
---
'astro': minor
---
Escape HTML inside of expressions by default. Please see [our migration guide](https://docs.astro.build/en/migrate/#deprecated-unescaped-html) for more details.

View file

@ -58,7 +58,7 @@
"test:match": "mocha --timeout 20000 -g" "test:match": "mocha --timeout 20000 -g"
}, },
"dependencies": { "dependencies": {
"@astrojs/compiler": "^0.12.0-next.8", "@astrojs/compiler": "^0.12.0-next.9",
"@astrojs/language-server": "^0.8.6", "@astrojs/language-server": "^0.8.6",
"@astrojs/markdown-remark": "^0.6.4", "@astrojs/markdown-remark": "^0.6.4",
"@astrojs/prism": "0.4.0", "@astrojs/prism": "0.4.0",
@ -95,10 +95,10 @@
"resolve": "^1.20.0", "resolve": "^1.20.0",
"rollup": "^2.64.0", "rollup": "^2.64.0",
"semver": "^7.3.5", "semver": "^7.3.5",
"sirv": "^2.0.2",
"serialize-javascript": "^6.0.0", "serialize-javascript": "^6.0.0",
"shiki": "^0.10.0", "shiki": "^0.10.0",
"shorthash": "^0.0.2", "shorthash": "^0.0.2",
"sirv": "^2.0.2",
"slash": "^4.0.0", "slash": "^4.0.0",
"sourcemap-codec": "^1.4.8", "sourcemap-codec": "^1.4.8",
"srcset-parse": "^1.1.0", "srcset-parse": "^1.1.0",

View file

@ -1,23 +1,6 @@
const entities = { '"': 'quot', '&': 'amp', "'": 'apos', '<': 'lt', '>': 'gt' } as const; const entities = { '"': 'quot', '&': 'amp', "'": 'apos', '<': 'lt', '>': 'gt' } as const;
const warned = new Set<string>(); export const escapeHTML = (string: any) => string.replace(/["'&<>]/g, (char: keyof typeof entities) => '&' + entities[char] + ';');
export const escapeHTML = (string: any, { deprecated = false }: { deprecated?: boolean } = {}) => {
const escaped = string.replace(/["'&<>]/g, (char: keyof typeof entities) => '&' + entities[char] + ';');
if (!deprecated) return escaped;
if (warned.has(string) || !string.match(/[&<>]/g)) return string;
// eslint-disable-next-line no-console
console.warn(`Unescaped HTML content found inside expression!
The next minor version of Astro will automatically escape all
expression content. Please use the \`set:html\` directive.
Expression content:
${string}`);
warned.add(string);
// Return unescaped content for now. To be removed.
return string;
};
/** /**
* RawString is a "blessed" version of String * RawString is a "blessed" version of String

View file

@ -8,7 +8,7 @@ import { escapeHTML, UnescapedString, unescapeHTML } from './escape.js';
export type { Metadata } from './metadata'; export type { Metadata } from './metadata';
export { createMetadata } from './metadata.js'; export { createMetadata } from './metadata.js';
export { escapeHTML, unescapeHTML } from './escape.js'; export { unescapeHTML } from './escape.js';
const voidElementNames = /^(area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/i; const voidElementNames = /^(area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/i;
const htmlBooleanAttributes = const htmlBooleanAttributes =
@ -36,7 +36,7 @@ async function _render(child: any): Promise<any> {
// of wrapping it in a function and calling it. // of wrapping it in a function and calling it.
return _render(child()); return _render(child());
} else if (typeof child === 'string') { } else if (typeof child === 'string') {
return escapeHTML(child, { deprecated: true }); return escapeHTML(child);
} else if (!child && child !== 0) { } else if (!child && child !== 0) {
// do nothing, safe to ignore falsey values. // do nothing, safe to ignore falsey values.
} }

View file

@ -99,4 +99,13 @@ describe('Expressions', () => {
// test 9: Expected {undefined && <span id="undefined" />} not to render // test 9: Expected {undefined && <span id="undefined" />} not to render
expect($('#frag-undefined')).to.have.lengthOf(0); expect($('#frag-undefined')).to.have.lengthOf(0);
}); });
it('Escapes HTML by default', async () => {
const html = await fixture.readFile('/escape/index.html');
const $ = cheerio.load(html);
expect($('body').children()).to.have.lengthOf(1);
expect($('body').text()).to.include('&lt;script&gt;console.log(&quot;pwnd&quot;)&lt;/script&gt;')
expect($('#trusted')).to.have.lengthOf(1);
});
}); });

View file

@ -0,0 +1,9 @@
<html lang="en">
<head>
<title>My site</title>
</head>
<body>
{'<script>console.log("pwnd")</script>'}
<Fragment set:html={'<script id="trusted">console.log("yay!")</script>'} />
</body>
</html>

11
pnpm-lock.yaml generated
View file

@ -340,7 +340,7 @@ importers:
packages/astro: packages/astro:
specifiers: specifiers:
'@astrojs/compiler': ^0.12.0-next.8 '@astrojs/compiler': ^0.12.0-next.9
'@astrojs/language-server': ^0.8.6 '@astrojs/language-server': ^0.8.6
'@astrojs/markdown-remark': ^0.6.4 '@astrojs/markdown-remark': ^0.6.4
'@astrojs/parser': ^0.22.1 '@astrojs/parser': ^0.22.1
@ -414,7 +414,7 @@ importers:
yargs-parser: ^21.0.0 yargs-parser: ^21.0.0
zod: ^3.8.1 zod: ^3.8.1
dependencies: dependencies:
'@astrojs/compiler': 0.12.0-next.8 '@astrojs/compiler': 0.12.0-next.9
'@astrojs/language-server': 0.8.10 '@astrojs/language-server': 0.8.10
'@astrojs/markdown-remark': link:../markdown/remark '@astrojs/markdown-remark': link:../markdown/remark
'@astrojs/prism': link:../astro-prism '@astrojs/prism': link:../astro-prism
@ -1328,10 +1328,12 @@ packages:
leven: 3.1.0 leven: 3.1.0
dev: true dev: true
/@astrojs/compiler/0.12.0-next.8: /@astrojs/compiler/0.12.0-next.9:
resolution: {integrity: sha512-HeREaw5OR5J7zML+/LxhrqUr57571kyNXL4HD2pU929oevhx3PQ37PQ0FkD5N65X9YfO+gcoEO6whl76vtSZag==} resolution: {integrity: sha512-XHvGrPBhr/LBYZT4TuXf8mK2+CYCClQoPln9FlF6gIx4yTsWpUwAi3mhhFOGJi4NB1Y1ZbcXS60IjMy/2adpLg==}
dependencies: dependencies:
tsm: 2.2.1
typescript: 4.6.2 typescript: 4.6.2
uvu: 0.5.3
dev: false dev: false
/@astrojs/language-server/0.8.10: /@astrojs/language-server/0.8.10:
@ -8788,6 +8790,7 @@ packages:
/source-map/0.6.1: /source-map/0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
requiresBuild: true
/source-map/0.7.3: /source-map/0.7.3:
resolution: {integrity: sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==} resolution: {integrity: sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==}