假设我有一个字符串键和字符串值的对象,我想将它们作为 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粉8981078742023-09-07 21:34:20
我们实际上可能只使用正则表达式和一些其他算法,而不必依赖于一种特定的语言,希望这是您所需要的。
通过声明对象键位于 [a-zA-Z0-9_-]
内,我们需要以某种方式解析值。
因此,我们可以将其分为几类,然后看看我们会遇到什么(为了清楚起见,它们可能会稍微简化):
'.*'
(用撇号包围的字符串;贪婪)".*"
(用双引号括起来的字符串;贪婪)[+-]?\d+(\.\d+)?(%|[A-z]+)?
(整数和小数,可选百分比或带单位)#[0-9A-f]{3,6}
(颜色)[A-z0-9_-]+
(关键字、命名颜色、“缓入”等内容)([\w-]+)\([^)]+\)
(类似 url()
、calc()
的函数> 等等)我可以想象在尝试识别这些模式之前您可以进行一些过滤。也许我们首先修剪值字符串。正如您所提到的, <<
和 >
可以在 preprocessPairs()
函数的开头进行转义,因为它不会出现为我们上面有的任何模式。如果您不希望在任何地方出现未转义的分号,您也可以转义它们。
然后我们可以尝试识别值中的这些模式,对于每个模式,我们可能需要再次运行过滤。我们期望这些模式将由一些(或两个)空白字符分隔。
包括对多行字符串的支持应该没问题,这是一个转义的换行符。
我们需要认识到我们至少要过滤两个上下文 - HTML 和 CSS。当我们在 元素中包含样式时,输入必须是安全的,同时它必须是有效的 CSS。幸运的是,您没有将 CSS 包含在元素的
style
属性中,因此这会稍微容易一些。
因此第 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; }
请评论、讨论、批评,如果我忘记触及您特别感兴趣的某个话题,请告诉我。
免责声明:我不是以下提到的来源的作者、所有者、投资者或贡献者。我只是碰巧用它们来获取一些信息。