我有一个Web应用程序,允许用户评论彼此的帖子。我们使用jQuery.ajax()
将新注释发送到服务器,它似乎在我们的测试中可靠地工作。
jQuery(".post form.add-comment").on("submit", function(event) {
event.preventDefault();
jQuery.ajax({
type: "POST",
url: "/comment",
data: jQuery(this).serialize()
});
});
但是,我们会自动从用户那里收集客户端JavaScript错误日志(使用Sentry),偶尔会出现如下错误:
URIError: malformed URI sequence jquery.min.js:4:25041
此错误似乎阻止将注释发送到我们的服务器,因此我们无法分辨用户尝试发布的可能导致此错误的内容。
可能导致此错误发生的原因,我们如何防止它?
出于这样或那样的原因,some of your users正在尝试提交包含我们可能称之为“无效字符”的评论。从\uD800
到\uDFFF
的Unicode代码点是保留的,因此UCS-2和UTF-16文本编码可以使用它们的对来识别其他有效的Unicode字符代码点,否则这些代码点将超出这些编码的范围。对于大多数现代编码,包括UTF-16,这些代码点只允许在有效对中出现,在转换为另一种编码时可以映射到有效的字符代码点;它们永远不会作为独立的“人物”存在。
遗憾的是,JavaScript在UTF-16标准化之前选择了UCS-2,而UCS-2确实允许您自己包含代理字符,而不需要配对以生成有效的代码点。因为JavaScript允许它,浏览器也接受它作为输入。这是一个复杂的问题,但在大多数情况下,它实际上并没有像用户体验到的那样。如果您的表单未使用JavaScript,则您的用户可以提交包含未配对代理的评论而不会出现错误。那会怎么样?
浏览器采用一种通用的方法来编码不兼容性:任何无法转换为目标编码的字符都被�
替换为Unicode替换字符\uFFFD
。在编码用于提交的典型表单数据时,浏览器会自动执行此替换。但是,jQuery.serialize()
没有任何这样的逻辑,并且它调用的内置encodeURIComponent
函数也没有编码形式值。相反,它只是抛出你所看到的URIError
。您可以在ECMAScript 9规范的Section 18.2.6.1.1: Runtime Semantics: Encode
中找到此错误。
encodeURIComponent('\uD83D') // URIError: malformed URI sequence
要在JavaScript中重现类似浏览器的表单行为,您需要查找并替换任何在\uD800
到\uDBFF
范围内出现“高代理”但没有在\uDC00
到\uDFFF
范围内跟随“低代理”的情况,或者反之亦然。这可能看起来像这样:
const replaceUnpairedSurrogates = s => s
.replace(/[\uD800-\uDBFF]+([^\uDC00-\uDFFF]|$)/g, '�$1')
.replace(/(^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]+/g, '$1�');
(此函数满足Unicode标准所要求的“转换过程的约束”,因为它确保后续有效字符不会被替换所破坏。它不符合可选的“最大子部分替换”约定,因为它可能会连续崩溃不成对的代理人物到一个替换人物。)
您目前正在使用jQuery.serialize(this)
对表单数据进行编码,这不允许我们在编码之前转换表单值。但jQuery.serialize(this)
和jQuery.param(jQuery.serializeArray(this))
一样,给我们一个申请替换的地方:
jQuery(".post form.add-comment").on("submit", function(event) {
event.preventDefault();
const data = jQuery.param(
jQuery.serializeArray(this).map(
({name, value}) => {
name: replaceUnpairedSurrogates(name),
value: replaceUnpairedSurrogates(value),
})
)
);
jQuery.ajax({
type: "POST",
url: "/comment",
data: data
});
});
为了进行测试,您可以运行以下命令来显示一个“无效字符”进行复制:
prompt('Copy this:', '\uD83D');