For AI agents: the complete documentation index is available at /tc39-atlas/llms.txt, the full documentation bundle is available at /tc39-atlas/llms-full.txt, and this page is available as Markdown at /tc39-atlas/proposals/stage/unstaged/distinguishing-literal-strings.md.
  • 简体中文
  • Distinguishing literal strings ?

    中文标题:区分字符串字面量

    提案概览
    提案速览

    该提案通过引入语言级钩子来区分字符串字面量和动态字符串,解决了基于 DOM 的 XSS 攻击问题,从而在客户端强制实施类似于 Google Closure 编译器的安全约束。它提出了两种方法:一种用于 WebIDL 的新 LiteralString 类型,以及仅接受字面量模板字符串的严格标签函数。该提案处于早期探索阶段,存在多个开放问题,如字面性的跟踪和 API 细节。

    Note

    以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。

    脚本中的字面量

    动机

    HTML 的 DOM 提供了多种将任意字符串转换为标记(.innerHTML = ...)或代码(scriptEl.innerText = ...el.onclick = ... 等)的机制。这些机制中的每一个都可能成为 XSS 的汇聚点,使攻击者能够将代码注入到未预期的上下文中,导致一类我们非常希望避免的基于 DOM 的 XSS 攻击。

    解决这个问题的一种方法(在谷歌等公司效果很好)是放弃基于字符串的 API,转向更强类型的接口,该接口可以在字符串进入应用程序时强制执行一定程度的验证和净化。如果开发人员能够将自己锁定在这样的系统中,那么他们就可以减少对每个已知 XSS 汇聚点进行深度审计的必要性,而将精力集中在生成如 SafeHtmlSafeUrl 这样的类型化对象的代码上。Trusted Types 提案旨在实现这一点。

    在很大程度上,这种机制完全可以在 DOM 和 WebIDL 中实现,而无需触及底层语言。然而,谷歌内部的安全审查人员发现的一个关键区别,目前在没有一些语言级钩子(hook)的情况下,无法在网络上复制。

    Closure 编译器可以区分嵌入到程序中的字符串字面量和作为某个操作(方法调用、属性 getter 等)结果的字符串。它对 goog.string.Const 强制约束,确保此类对象只能从字面量构造,这 a) 在谷歌的生产代码中显然相当普遍,并且 b) 可证明是安全的(攻击者无法在不直接注入创建此类字面量的脚本的情况下控制字面量的值)。

    也就是说,开发人员可能会通过首先创建一个 goog.string.Const,然后在工厂方法中使用它来创建 SafeUrl 对象,如下所示:

    const url = goog.string.Const.from("https://safe.test/totally/safe/url");
    return SafeUrl.fromConstant(url);

    如果我们能在平台中做出这种断言,而不是完全依赖编译时检查,那就太好了。客户端断言可以增加检查的健壮性,提供纵深防御,并为那些未经编译就通过发布流程的代码提供一个安全网。

    提案

    老实说,我对语言内部的了解不够,无法提出有理有据的提案。相反,我有上述用例,以及我对平台方面期望的模糊草图。我希望从真正了解语言的人那里获得关于细节的反馈,即语言可能愿意并能够提供什么。

    考虑到这一点,以下是对可能或不可能合理的事情的大胆推测!

    字面量字符串类型

    提供这种能力的一种方式是暴露一种与字面量字符串对应的新字符串类型,WebIDL 可以在此基础上,在执行类型检查时区分字面量。也就是说,今天,我们可以生成如下 WebIDL 片段:

    interface TrustedHTML {
      static TrustedHTML escape(DOMString html);
    };

    它可以被调用为 TrustedHTML.escape("Literal string!")TrustedHTML.escape(formField.value),并根据需要转义字符串。

    如果能添加一些类似这样的内容,那就理想了:

    interface TrustedHTML {
      static TrustedHTML createFromLiteral(LiteralString html);
    };

    它可以被调用为 TrustedHTML.createFromLiteral("Literal string!"),但调用 TrustedHTML.createFromLiteral(formField.value) 将抛出 TypeError

    同样理想的是,我们允许字面量与其他字面量组合(这在具有约定行长度限制的代码库中很常见)。也就是说,我们允许以下内容:

    TrustedHTML.createFromLiteral("A marginally longer literal string that seems to keep going " +
                                  "and going and going and going. Wow, what a long string.");

    同样理想的是,我们跟踪字符串的字面性。也就是说,我们允许以下内容:

    let a = "Literal string!";
    TrustedHTML.createFromLiteral(a);
    
    let b = "Another literal!";
    TrustedHTML.createFromLiteral(a + b);
    
    let c = a + b;
    TrustedHTML.createFromLiteral(c);

    严格标签函数

    想到的一个替代方案是在带标签的模板字符串之上构建类似的系统。也就是说,你可以想象如下内容:

    function trustedUrlizer(templateString) {
      return TrustedUrl.unsafelyCreate(templateString);
    }
    
    return trustedUrlizer`https://safe.test/totally/safe/url`;

    如果我们还有一种机制来确保标签函数 接受模板字符串,那将非常棒。也就是说,trustedUrlizer(formField.value) 将失败,也许抛出 TypeError

    Daniel Ehrenberg 在 mikewest/tc39-proposal-literals#2 中对此进行了细化, 建议:

    这是我设想的 API 表面:模板标签的第一个参数将具有一个额外的属性 literal,它指示提供给模板的参数是否为字面量。我们可以通过将其表示为内部槽,并将 literal 作为自有 getter 来使其不可伪造,这样你可以做类似以下的事情来防止攻击者使用具有 literal: true 属性的任何旧对象来调用你的模板:

    // 不可猴子补丁的方式来获取 getter
    let getLiteral = Object.getOwnPropertyDescriptor((_ => _)``, "literal").get; 
    
    function literalString(strings, ...keys) {
      if (!getLiteral.call(strings)) throw new Error();
      return String.raw(strings, ...keys);
    }

    这个 literalString 模板标签类似于 String.raw,但如果你不传递字面量,它将抛出异常。它输出一个普通字符串,不再留下它是字面量的痕迹。这个两行代码可能是推荐的启动需要字面量字符串的调用的方式。不可能破坏字符串字面量的内容,因为 strings 对象(及其内部的 raw 对象)是冻结的。

    对此,我只想补充一点,我们希望确保字面性以能够在 WebIDL 中使用的方式暴露出来,以便我们能够对内置标签函数强制执行这种类型的检查,但这似乎会自然地从这个提案中产生。

    常见问题解答

    • 人们难道不能通过 Closure 或其他编译时检查自己完成这项工作吗?

      是的,事实上谷歌今天就在内部这样做。参与该构建过程的人们希望在已经进行的构建时分析之上添加客户端检查,以使其更健壮。上面提到的 Trusted Types 提案也将受益于 fromLiteral 构造机制,因为来自谷歌代码库的轶事数据表明,该机制既安全又广泛可用。

    • 对于第一个提案,我们需要在多大程度上跟踪字面性?

      一个好问题!我持灵活态度!

      我们_不_需要跟踪的一件事是将字面量用作对象键。也就是说,我完全乐意将 { "a": "value" }{ a: "value" }obj["a"] = "value" 视为具有相同键的值。

    先前技术