无需 JavaScript 即可安全地将不受信任的输入分配给 CSS 自定义属性:指南
P粉356361722
P粉356361722 2023-09-06 22:32:52
0
1
740

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

我所说的安全是指

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

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

通过阅读 CSS 规范和一些个人测试,我认为通过以下步骤获取值可以取得很大的进展:

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

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

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


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

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;
}

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

P粉356361722
P粉356361722

全部回复(1)
P粉898107874

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

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

价值模式

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

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

第一次过滤

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

识别模式

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

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

语言环境

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

基于值模式的过滤

  1. 由撇号包围的字符串 - 除了撇号和分号之外我们不关心任何东西,因此我们需要在字符串中找到这些字符的未转义实例并对它们进行转义
  2. 同上,只是用双引号
  3. 应该没问题
  4. 应该没问题
  5. 基本没问题
  6. 这是有趣的部分

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

但与其他点相比,我认为相对更大的挑战是第 6 点。您可能决定简单地禁止此自定义样式中的 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;
}

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

来源

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

热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板