isTemplateObject ?
中文标题:isTemplateObject(模板对象检测)
提案速览
该提案引入了 Reflect.isTemplateObject 方法,使模板标签函数能够判断自己是以真正的模板字符串 bundle 被调用,还是被传入了一个伪造的数组。这有助于区分受信任的、由开发者编写的字符串与可能受攻击者控制的数值,从而更安全地处理敏感操作。
Note
以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。
Reflect.isTemplateObject(已撤回)
作者:@mikesamuel、@koto
提案负责人:@littledan、@ljharb
审阅者:@erights、@jridgewell
为模板标签函数提供了一种方式,用以判断它们是否是以模板字符串 bundle 的形式被调用的。
目录
使用场景与先前讨论
区分来自可信开发者的字符串与可能由攻击者控制的字符串
WICG/trusted-types#96 描述了一个场景:模板标签假定其中的字面量字符串由可信开发者编写,而插值内容则可能是不可信的。
result = sensitiveOperation`trusted0 ${ untrusted } trusted1`
// Authored by dev ^^^^^^^^ ^^^^^^^^
// May come from outside ^^^^^^^^^
该提案将提供足够的上下文,以便在情况并非如此时发出警告或报错。
function (trustedStrings, ...untrustedArguments) {
if (Reflect.isTemplateObject(trustedStrings)
// instanceof provides a same-Realm guarantee for early frozen objects.
&& trustedStrings instanceof Array) {
// Proceed knowing that trustedStrings come from
// the JavaScript module's authors.
} else {
// Do not trust trustedStrings
}
}
这里假设攻击者无法将字符串传递给 eval 或 new Function,如下所示:
const attackerControlledString = '((x) => x)`evil string`';
// 天真代码
let x = eval(attackerControlledString)
console.log(Reflect.isTemplateObject(x));
如果攻击者能够执行任意代码,许多其他安全假设都会失效,因此该检查仍然有用。
一个示例
下面示例展示了 isTemplateObject 如何让标签函数明智地使用敏感操作,即*创建受信任类型*。
由于敏感操作位于局部作用域内,标签函数的调用者无法直接访问。
这里假设 TT 的先到先得名称限制能够解决配置问题,只允许被授权的调用者访问该敏感操作。
const { Array, Reflect, TypeError } = globalThis;
const { createPolicy } = trustedTypes;
const { isTemplateObject } = Reflect;
const { error: consoleErr } = console;
/**
* A tag function that produces *TrustedHTML* or null if the
* policy name "trustedHTMLTagFunction" is not available.
*/
export trustedHTML = (() => {
// We use TrustedType's first-come-first-serve policy name restrictions
// to provision this scope with sensitiveOperation.
const policyName = 'trustedHTMLTagFunction';
let policy;
try {
policy = createPolicy(
'trustedHTMLTagFunction',
{ createHTML(s) { return s } }
);
} catch (ex) {
consoleErr(`${policyName} is not an allowed trustedTypes policy name`);
return null;
}
// This is the sensitive operation.
const { createHTML } = policy;
// This tag function uses isTemplateObject to reject strings that
// do not appear in user code in the same realm.
//
// With a reliable isTemplateObject check, the attack surface is
// <= |set of template applications in trusted code|.
//
// That set is finite.
//
// Without a reliable isTemplateObject check, the attack surface is
// <= |set of attacker controlled strings|. That is, in practice,
// unbounded.
//
// This assumes no attacker has eval.
const trustedHTMLTagFunction = (strings) => {
if (isTemplateObject(strings) && strings instanceof Array) {
return createHTML(strings.raw[0]);
}
throw new TypeError("Expected template object");
};
// With the check it's safe to export this tag function that closes
// over a sensitive operation to anyone.
return trustedHTMLTagFunction;
})()
如果没有 isArrayTemplate,该检查可以被绕过:
// A naive, but non-malicious function.
function f(x) {
// People trust trustedHTMLTagFunction.
// Our HTML is trustworthy because <bad argument> so we'll just
// piggyback off that by using a value that looks like a template object.
// What could possibly go wrong?
const s = dodgyMarkdownToHTMLConverter(x);
const pseudoTemplateObject = [s];
pseudoTemplateObject.raw = Object.freeze([s]);
return trustedHTML(Object.freeze(pseudoTemplateObject));
}
// An attacker controlled string reaches f().
const payload = '<img onerror=alert(document.origin) src=x>';
console.log(`f(${ JSON.stringify(payload) }) = ${ f(payload) }`);
这里的威胁模型涉及三个角色:
- 第一方开发者团队(与安全专家合作)决定信任该标签函数。
- 恶意的攻击者控制变量
payload 中的字符串。
- 非恶意但易被混淆的第三方库试图通过伪造模板对象来提供更高级别的服务。它假设其客户愿意信任
dodgyMarkdownToHTMLConverter 为当前源生成 HTML。
当第一方开发者对风险的容忍度低于最寻求风险的第三方依赖对 HTML 注入的容忍度时,我们已经针对这一威胁模型进行了处理。
这个简单的实现没有处理插值。
更彻底的实现可以使用上下文自动转义。
这不是什么
这并非试图判断当前函数是否是作为模板字面量被调用的。
关于为什么这站不住脚,请参阅所链接的问题。尤其是关于威胁模型、eval 和尾调用优化的讨论,这些讨论对替代方案不利。
可能的规范语言
您可以浏览 ecmarkup 输出 或浏览 源文件。
测试
test262
草稿测试
将被添加到
test/built-ins/Reflect
相关工作
如果 literals 提案 得以推进,本提案就没有必要了,因为两者都涵盖了本文档中的使用场景。