如何在没有 JavaScript 的情况下安全地将不受信任的输入分配给 CSS 自定义属性?

How to safely assign untrusted input to CSS custom properties without JavaScript?

提问人:bigblind 提问时间:5/10/2023 最后编辑:bigblind 更新时间:5/11/2023 访问量:65

问:

假设我有一个字符串键和字符串值的对象,我想将它们作为 CSS 自定义属性写入服务器生成的一些 HTML。我怎样才能安全地这样做?

安全地说,我的意思是

  • 如果可能的话,自定义属性声明不应导致 CSS 语法错误,从而阻止浏览器正确解析其他样式声明或 HTML 文档的某些部分。如果由于某种原因无法做到这一点,则应省略键值对。
  • 更强烈的是,应该不可能使用它进行跨站点脚本。

为了简单起见,我将限制键以仅允许类中的字符。[a-zA-Z0-9_-]

通过阅读CSS规范和一些个人测试,我认为我可以通过以下步骤获取价值来走得很远:

  • 查找字符串
  • 确保每个引号后面都跟着另一个相同类型(“或')的(未转义)引号。如果不是这种情况,请放弃此键/值对。
  • 确保字符串外的每个左大括号都有一个匹配的右大括号。如果没有,请丢弃此键值对。{([
  • 转义 with 的所有实例和 with 的所有实例。<\3C>3E
  • 转义 with 的所有实例。;\3B

我根据这个CSS语法规范想出了上面的步骤

对于上下文,这些属性可以由我们在其他地方插入的用户自定义样式使用,但同一对象也用作模板中的模板数据,因此它可能包含用作内容的字符串和用作 CSS 变量的字符串的混合。我觉得上面的算法在非常简单之间取得了很好的平衡,但也不会冒险丢弃太多可能在 CSS 中有用的键值对(即使考虑将来对 CSS 的添加,但我想确保我没有遗漏一些东西。


这里有一些JS代码,显示了我想要实现的目标。 是有问题的对象,并且是一个函数,它接受对象并对其进行预处理,删除/重新格式化值,如上述步骤所述。objpreprocessPairs

function generateThemePropertiesTag(obj) {
  obj = preprocessPairs(obj);
  return `<style>
:root {
${Object.entries(obj).map(([key, value]) => {
  return `--theme-${key}: ${value};`
}).join("\n")}
}
</style>`
}

所以当给定这样的对象时

{
  "color": "#D3A",
  "title": "The quick brown fox"
}

我希望CSS看起来像这样:

:root {
--theme-color: #D3A;
--theme-title: The quick brown fox;
}

虽然在CSS中使用是一个非常无用的自定义变量,但它实际上并没有破坏样式表,因为CSS忽略了它不理解的属性。--theme-title

HTML CSS 清理

评论

0赞 Tommander 5/10/2023
您有一个字符串键和字符串值的对象...究竟在哪里?在文件中?或者我们是在谈论某种语言?你对HTML做了什么预处理?我需要更多关于您希望实现此目标的环境的更多背景信息。谢谢:)
0赞 bigblind 5/10/2023
服务器(或者至少是实现的部分是用 JavaScript 编写的,但我认为这个问题足够通用,可以用其他语言解决。不过,为了清楚起见,我还是会添加一些伪 JS,以表达我所期望的问题。
0赞 bigblind 5/10/2023
我用一个 JS 示例更新了我的问题,该示例说明了我希望最终代码是什么样子,并带有一些示例输入和输出。
1赞 Tommander 5/10/2023
非常感谢,明白了。因此,您基本上正在研究如何清理 CSS 属性值,以便每个规范的所有值都通过,但那些不正确的值或例如“url('badboy.com/bad.css”)不会破坏它/使其易受攻击。简而言之,您正在尝试将 w3.org/TR/css-values-3 压缩成一段包含安全性的短代码。
0赞 bigblind 5/10/2023
是的,在可能的范围内

答:

1赞 Tommander 5/10/2023 #1

我们实际上可能只使用正则表达式和其他一些算法,而不必依赖一种特定的语言,希望这是您在这里需要的。

通过声明对象键在 in 中,我们需要以某种方式解析值。[a-zA-Z0-9_-]

值模式

因此,我们可以将其分为几类,看看我们会遇到什么(为了清楚起见,它们可能会稍微简化):

  1. '.*'(被撇号包围的字符串;贪婪)
  2. ".*"(用双引号括起来的字符串;贪婪)
  3. [+-]?\d+(\.\d+)?(%|[A-z]+)?(整数和十进制数,可选百分比或单位)
  4. #[0-9A-f]{3,6}(颜色)
  5. [A-z0-9_-]+(关键字、命名颜色、“ease-in”之类的东西)
  6. ([\w-]+)\([^)]+\)(函数,如等)url()calc()

第一次过滤

我可以想象,在尝试识别这些模式之前,您可以进行一些过滤。也许我们先修剪值字符串。正如你提到的,并且可以在函数的开头进行转义,因为它不会作为我们上面的任何模式的一部分出现。如果你不希望在任何地方出现未转义的分号,你也可以转义它们。<>preprocessPairs()

识别模式

然后,我们可以尝试在中识别这些模式,对于每个模式,我们可能需要再次运行过滤。我们预计这些模式将由一些空格字符(或两个)分隔。

包括对多行字符串的支持应该是可以的,这是一个转义的换行符。

语言语境

我们需要认识到,我们至少过滤了两个上下文 - HTML 和 CSS。由于我们在元素中包含样式,因此输入必须是安全的,同时它必须是有效的 CSS。幸运的是,您没有在元素的属性中包含 CSS,因此这会稍微容易一些。<style>style

基于值模式的筛选

  1. 被撇号包围的字符串 - 除了撇号和分号之外,我们什么都不关心,所以我们需要在字符串中找到这些字符的未转义实例并转义它们
  2. 同上,只是用双引号
  3. 应该没问题
  4. 应该没问题
  5. 差不多还可以
  6. 这是有趣的部分

因此,第 1-5 点将非常容易,并且使用前面的简单过滤和修剪将涵盖大部分值。通过一些添加(不知道对性能有什么影响),它甚至可以对正确的单位、关键字等进行额外的检查。

但与其他点相比,我看到一个相对更大的挑战是点#6。您可以决定简单地禁止使用此自定义样式,让您检查函数的输入,例如,您可能希望转义分号,甚至可能通过微小的调整再次检查函数内部的模式,例如 for .url()calc()

结论

从我的角度来看,这是从我的角度来看的。通过对这些正则表达式进行一些调整,它应该能够补充您已经完成的工作,并为输入 CSS 提供尽可能多的灵活性,同时让您不必在每次调整 CSS 功能时调整代码。

function preprocessPairs(obj) {
  // Catch-all regular expression
  // Explanation:
  // (                                   Start of alternatives
  //   \w+\(.+?\)|                       1st alternative - function
  //   ".+?(?<!\\)"|                     2nd alternative - string with double quotes
  //   '.+?(?<!\\)'|                     3rd alternative - string with apostrophes
  //   [+-]?\d+(?:\.\d+)?(?:%|[A-z]+)?|  4th alternative - integer/decimal number, optionally per cent or with a unit
  //   #[0-9A-f]{3,6}|                   5th alternative - colour
  //   [A-z0-9_-]+|                      6th alternative - keyword
  //   ''|                               7th alternative - empty string
  //   ""                                8th alternative - empty string
  // )
  // [\s,]*
  const regexA = /(\w+\(.+?\)|".+?(?<!\\)"|'.+?(?<!\\)'|[+-]?\d+(?:\.\d+)?(?:%|[A-z]+)?|#[0-9A-f]{3,6}|[A-z0-9_-]+|''|"")[\s,]*/g;

  // newObj contains filtered testObject
  const newObj = {};

  // Loop through all object properties
  Object.entries(obj).forEach(([key, value]) => {
    // Replace <>;
    value = value.trim().replace('<', '\\00003C').replace('>', '\\00003E').replace(';', '\\00003B');

    // Use catch-all regex to split value into specific elements
    const matches = [...value.matchAll(regexA)];

    // Now try to build back the original value string from regex matches.
    // If these strings are equal, the value is what we expected.
    // Otherwise it contained some unexpected markup or elements and should
    // be therefore discarded.
    // We specifically set to ignore all occurences of url() and @import
    let buildBack = '';
    matches.forEach((match) => {
      if (Array.isArray(match) && match.length >= 2 && match[0].match(/url\(.+?\)/gi) === null && match[0].match(/@import/gi) === null) {
        buildBack += match[0];
      }
    });

    console.log('Compare\n');
    console.log(value);
    console.log(buildBack);
    console.log(value === buildBack);

    if (value === buildBack) {
      newObj[key] = value;
    }
  });

  return newObj;
}

请评论、讨论、批评,如果我忘了触及你特别感兴趣的话题,请告诉我。

来源

免责声明:我不是以下来源的作者、所有者、投资者或贡献者。我只是碰巧用它们来获取一些信息。

评论

0赞 bigblind 5/10/2023
哇,惊人的文章!
0赞 bigblind 5/10/2023
我可能还允许在这些匹配之间的任何位置使用逗号来支持这样的情况,您可以在其中指定多个框阴影: jsfiddle.net/c90mg2vh/2
0赞 Tommander 5/11/2023
你是绝对正确的。我想出了类似 的东西,它实际上可以在某种程度上完成这项工作,无论是用逗号还是空格字符分隔。正如我所说,仍在为此编写测试,但到目前为止,对于一些简单的情况以及带有转义和未转义(双)引号的字符串的疯狂组合,这看起来很有希望。/(\w+\(.+?\)|".+?(?<!\\)"|'.+?(?<!\\)'|[+-]?\d+(\.\d+)?(%|[A-z]+)?|#[0-9A-f]{3,6}|[A-z0-9_-]+|''|"")[\s,]*/g
0赞 Tommander 5/11/2023
正如所承诺的那样,我编辑了答案,以包含一个函数的实现示例,该示例可能会执行我在答案中描述的功能。preprocessPairs()