关于 HTML 元素的 Attribute 的转义问题

陪她去流浪 桃子 2024年05月24日 编辑 阅读次数:466

说起来,这么多年,我一直没有彻底搞清楚过。比如,哪些字符需要转义,中文要不要转义,URL 呢?

对于第一个问题:

我在评论功能的代码里面已经解释得比较清楚了(之所以需要这部分代码而没有使用 innerText/innerHTML 的方式让浏览器帮我正确地转义,是因为目前部分内容还是通过手动拼接 HTML 的方式构成的,需要找时间换成 <template/> 以及 ShadowDOM):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 把可能的 HTML 特殊字符转义以作为纯文本嵌入到页面中。
// 单、双引号均没必要转换,任何时候都不会引起歧义。
const h2t = (h) => {
	const map = {'&': '&amp;', '<': '&lt;', '>': '&gt;'};
	return h.replace(/[&<>]/g, c => map[c]);
};
// 转义成属性值。
// 两种情况:手写和非手写。
// 手写的时候知道什么时候需要把值用单、双引号包起来,跟本函数无关。
// 如果是构造 HTML,则(我)总是放在单、双引号中,所以 < > 其实没必要转义,
// 而如果可能不放在引号中,则需要转义。' " 则总是需要转义。
// 试了一下在火狐中执行 temp0.setAttribute('title', 'a > b'),不管是查看或者编辑,都没被转义。
// https://mina86.com/2021/no-you-dont-need-to-escape-that/
const h2a = (h) => {
	const map = {'&': '&amp;', "'": '&#39;', '"': '&quot;'};
	return h.replace(/[&'"]/g, c => map[c]);
};

对于后面两个问题,一开始觉得按照第一步做完就已经足够了,原因:转义的目的只是为了不导致 HTML 解析的时候出现歧义,并没有其它目的。比如:防止 Attribute.Value 被中的引号/大于/小于符号对标签的错误解析。上面👆代码中的链接非常清楚地解释了什么时候会引起歧义。

但是今天用 Go 的 "html/template" 渲染一段单测的时候,竟然挂了,就是下面这个:

1
2
template.Must(template.New(`t`).Parse(`<span src="{{.}}"></span>`)).Execute(os.Stdout, `/118/我的一个道姑朋友.mp3`)
template.Must(template.New(`t`).Parse(`<span xxx="{{.}}"></span>`)).Execute(os.Stdout, `/118/我的一个道姑朋友.mp3`)

结果如下:

1
2
<span src="/118/%e6%88%91%e7%9a%84%e4%b8%80%e4%b8%aa%e9%81%93%e5%a7%91%e6%9c%8b%e5%8f%8b.mp3"></span>
<span xxx="/118/我的一个道姑朋友.mp3"></span>

这让我有点儿意外,因为以前觉得: URL 在 Attribute 里面根本不会引起歧义,根本没必要转义,所以我就算是手写 HTML 的时候,也不会特意去转义(实际上,不转义大部分时候也不会有问题)。所以我在写单测的时候期待结果也不是转义的结果,然后就挂了。

然后就去找了找文档源码对于这方面的处理:

1
2
3
4
5
6
7
8
9
This package understands HTML, CSS, JavaScript, and URIs. It adds sanitizing
functions to each simple action pipeline, so given the excerpt

	<a href="/search?q={{.}}">{{.}}</a>

At parse time each {{.}} is overwritten to add escaping functions as necessary.
In this case it becomes

	<a href="/search?q={{. | urlescaper | attrescaper}}">{{. | htmlescaper}}</a>

解答了我的疑惑:确实会先 URL 转义,然后 Attribute 转义。 但,只针对特定的 Attribute Name。然后,我找到了这份列表:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
template (stable)pwd
/opt/homebrew/Cellar/go/1.22.2/libexec/src/html/template
template (stable) → cat attr.go | grep contentTypeURL
        "action":          contentTypeURL,
        "archive":         contentTypeURL,
        "background":      contentTypeURL,
        "cite":            contentTypeURL,
        "classid":         contentTypeURL,
        "codebase":        contentTypeURL,
        "data":            contentTypeURL,
        "formaction":      contentTypeURL,
        "href":            contentTypeURL,
        "icon":            contentTypeURL,
        "longdesc":        contentTypeURL,
        "manifest":        contentTypeURL,
        "poster":      contentTypeURL,
        "profile":     contentTypeURL,
        "src":         contentTypeURL,
        "usemap":      contentTypeURL,
        "xmlns":       contentTypeURL,

而 Go 的这份列表,引用自 W3C:

里面清晰地定义了,对于 URL,哪些字符需要转义、如何转义。 也可以参考维斯百科,也解释得很清楚,更简单。