ES2015模板字符串安全问题

ES2015 template strings security issue

本文关键字:安全 问题 字符串 ES2015      更新时间:2023-09-26

这是MDN的一句话:

模板字符串不得由不受信任的用户构造,因为他们有权访问变量和函数。

举个例子:

`${console.warn("this is",this)}`; // "this is" Window
let a = 10;
console.warn(`${a+=20}`); // "30"
console.warn(a); // 30

这里的例子没有显示我可以看到的任何漏洞。

谁能举一个利用这一点的漏洞利用的例子?

毫无意义。模板字符串无法访问任何内容,也不会执行。模板字符串是语言的语法元素。

因此,动态构造模板字符串没有问题 - 这就像构建表达式(无论格式如何,无论是代码字符串还是 AST)。MDN 暗示的问题是评估这样的表达式(例如使用 eval,将其序列化为提供给用户的脚本等) - 它可能包含任意代码,而不是字符串文字!但你当然不会那样做,对吧?

此警告类似于说"使用 + 运算符的串联不得由不受信任的用户构造,因为他们有权访问变量和函数",并给出示例"" + console.warn("this is",this) + ""。嗯,对于语言的任何表达都是如此,所以它不是特别有趣。


当我们谈论蹩脚的编码时,当然有一种情况是使用模板字符串(嘿,它们是多行的等等)而不是字符串文字可能会导致问题:

function escapeString(str) {
    return JSON.stringify(str).slice(1, -1)
           .replace(/'u2028/g, "''u2028").replace(/'u2029/g, "''u2029");
}
// This is (kinda) fine!
var statement = 'var x = "Hello,''n'+escapeString(userInput)+'";';
eval(statement); // some kind of evaluation
// But this is not:
var statement = 'var x = `Hello,'n'+escapeString(userInput)+'`;';
//                       ^                                   ^

现在想象一下userInput包含一个${…} - 我们没有逃脱......

我认为

@Bergi是正确的 - 这里的危险涉及使用eval或类似的方法来允许用户构造实际的模板字符串,而不是替换。

示例漏洞利用:懒惰的开发人员希望允许用户在他们的评论中执行一些字符串替换,例如在像 SO 这样的网站上引用其他用户或问题。他没有为此开发令牌,然后进行适当的解析和替换,而是决定接受这样的语法:

"I think ${firstPoster} is an idiot! See ${question(1234)} for details!"

并通过这样的函数运行它:

var firstPoster = {...};
function question() {...}
processInput(input) {
  return eval('`' + input + '`');
}

如果此代码在客户端上eval并显示给其他用户,则恶意用户可能会注入 XSS 攻击。如果它在服务器上eval,攻击者可以控制机器。

该示例似乎不再在 MDN 文档中。正如 Bergi 的回答所指出的那样,给定的示例似乎并没有突出模板字符串的任何特殊之处。

但是,在从对象构建字符串时,您绝对应该注意一个特殊的安全问题:

如果将 toString() 方法传递给字符串插值/串联表达式,则将在非字符串对象上隐式调用该方法。

可能还有其他隐式调用toString()的情况。但在我看来,字符串插值是最常见的插值之一;事实上,这是我经常经历的。例如,假设您以某种方式从外部某个地方接收对象,例如通过iframepostMessage。在这种情况下,您可能希望执行诸如将收到的消息记录到控制台之类的操作 - 并且您可能只想将对象直接传递到内插字符串中。

但是发送方(可能是攻击者)可以完全控制toString()的定义,并且可以在其中插入他们喜欢的任何代码。因此,一旦您将该对象传递给内插串联字符串,只要其他人控制所述对象的定义,您就容易受到攻击。

这里有一个简单的例子(添加到codesandbox中),表明即使对象在toString()中返回看似无害的字符串,它们确实可以做一些危险的事情,比如读取本地存储:

import "./styles.css";
localStorage.setItem("secret1", "sssh! One");
localStorage.setItem("secret2", "sssh! Two");
const evilObject1 = {
  toString() {
    alert("I stole a secret: " + localStorage.getItem("secret1"))
    return "I'm innocent";
  }
};
const evilObject2 = {
  toString() {
    alert("I stole a secret: " + localStorage.getItem("secret2"))
    return "I'm innocent";
  }
};
const strInter = `Seemingly innocent object, interpolated: ${evilObject1}`;
const strConcat = "Seemingly innocent object, concatenated: " + evilObject2;
let p = document.createElement("p");
p.innerHTML = strInter
let p2 = document.createElement("p");
p2.innerHTML = strConcat
document.body.appendChild(p);
document.body.appendChild(p2);

对于鸭子类型的Javascript,这是一个非常真实的漏洞,因为您可能认为您收到的对象(例如通过postMessage)是一个字符串,实际上,它的行为可能像字符串(因为它具有巧妙设计的toString()方法),但除非您动态检查类型,否则您不知道是否真的得到了字符串。

如果需要字符串,可以按如下方式修复上述漏洞:

const sanitized1 = typeof evilObject1 === 'string' ? evilObject1 : "BAD OBJECT1"
const sanitized2 = typeof evilObject2 === 'string' ? evilObject2 : "BAD OBJECT2"
const strInter = `Seemingly innocent object, interpolated: ${sanitized1}`;
const strConcat = "Seemingly innocent object, concatenated: " + sanitized2;

通过此修复,可以避免在不安全对象上隐式调用toString()。你得到的对象要么是字符串,这些将被使用,要么它们不是,你会得到"坏对象"文本。