假設(shè)我有一個字串鍵和字串值的對象,我想將它們作為 CSS 自訂屬性寫入伺服器產(chǎn)生的一些 HTML 中。我怎樣才能安全地做到這一點?
我所說的安全是指
為了簡單起見,我將限制鍵只允許 [a-zA-Z0-9_-]
類別中的字元。
透過閱讀 CSS 規(guī)範(fàn)和一些個人測試,我認為透過以下步驟來取得值可以取得很大的進展:
{([字串外部的
都有一個符合的右大括號。如果沒有,則丟棄此鍵值對。\3C
轉(zhuǎn)義 <<
的所有實例,以及所有使用 3E
轉(zhuǎn)義 >
的所有實例。 \3B
對 ;
的所有實例進行轉(zhuǎn)義。 我根據(jù)這個 CSS 語法規(guī)格想出了上述步驟
對於上下文,這些屬性可以由我們在其他地方插入的用戶自訂樣式使用,但同一物件也用作模板中的模板數(shù)據(jù),因此它可能包含旨在作為內(nèi)容的字串和預(yù)期的字串的混合作為CSS 變數(shù)。我覺得上面的演算法取得了很好的平衡,既非常簡單,又不會冒丟棄太多可能在CSS 中有用的鍵值對的風(fēng)險(即使考慮到未來對CSS 的添加,但我想確保我沒有遺漏什麼。
這裡有一些 JS 程式碼,展示了我想要實現(xiàn)的目標(biāo)。 obj
是有問題的對象,而 preprocessPairs
是一個函數(shù),它接受該對象並對其進行預(yù)處理,刪除/重新格式化值,如上述步驟所述。
function generateThemePropertiesTag(obj) { obj = preprocessPairs(obj); return `<style> :root { ${Object.entries(obj).map(([key, value]) => { return `--theme-${key}: ${value};` }).join("\n")} } </style>` }
所以當(dāng)給定一個這樣的物件時
{ "color": "#D3A", "title": "The quick brown fox" }
我希望 CSS 看起來像這樣:
:root { --theme-color: #D3A; --theme-title: The quick brown fox; }
雖然 --theme-title
在 CSS 中使用時是一個非常無用的自訂變量,但它實際上並沒有破壞樣式表,因為 CSS 會忽略它不理解的屬性。
我們實際上可能只使用正規(guī)表示式和一些其他演算法,而不必依賴特定的語言,希望這是您所需要的。
透過宣告物件鍵位於 [a-zA-Z0-9_-]
內(nèi),我們需要以某種方式解析值。
因此,我們可以將其分為幾類,然後看看我們會遇到什麼(為了清楚起見,它們可能會稍微簡化):
'.*'
(用撇號包圍的字串;貪婪)".*"
(用雙引號括起來的字串;貪婪)[ -]?\d (\.\d )?(%|[A-z] )?
(整數(shù)和小數(shù),可選百分比或帶單位)#[0-9A-f]{3,6}
(顏色)[A-z0-9_-]
(關(guān)鍵字、命名顏色、「緩入」等內(nèi)容)([\w-] )\([^)] \)
(類似url()
、calc()
的函數(shù)> 等等)我可以想像在嘗試識別這些模式之前您可以進行一些過濾。也許我們首先修剪值字串。正如您所提到的, 和
>
可以在preprocessPairs()
函數(shù)的開頭進行轉(zhuǎn)義,因為它不會出現(xiàn)為我們上面有的任何模式。如果您不希望在任何地方出現(xiàn)未轉(zhuǎn)義的分號,您也可以轉(zhuǎn)義它們。
然後我們可以嘗試識別值中的這些模式,對於每個模式,我們可能需要再次執(zhí)行過濾。我們期望這些模式將由一些(或兩個)空白字元分隔。
包括對多行字串的支援應(yīng)該沒問題,這是一個轉(zhuǎn)義的換行符。
我們需要認識到我們至少要過濾兩個上下文 - HTML 和 CSS。當(dāng)我們在 元素中包含樣式時,輸入必須是安全的,同時它必須是有效的 CSS。幸運的是,您沒有將 CSS 包含在元素的
style
屬性中,因此這會稍微容易一些。
因此第 1-5 點將非常簡單,透過前面的簡單過濾和修剪將覆蓋大部分值。透過一些添加(不知道對效能有什麼影響),它甚至可能會對正確的單位、關(guān)鍵字等進行額外的檢查。
但與其他點相比,我認為相對更大的挑戰(zhàn)是第 6 點。您可能決定簡單地禁止此自訂樣式中的url()
,讓您檢查函數(shù)的輸入,因此例如您可能想要轉(zhuǎn)義分號,甚至可能透過微小的調(diào)整再次檢查函數(shù)內(nèi)的模式例如對於calc()
。
總的來說,這是我的觀點。透過對這些正規(guī)表示式進行一些調(diào)整,它應(yīng)該能夠補充您已經(jīng)所做的工作,並為輸入 CSS 提供盡可能多的靈活性,同時使您不必在每次調(diào)整 CSS 功能時調(diào)整程式碼。
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; }
請評論、討論、批評,如果我忘記觸及您特別感興趣的某個主題,請告訴我。
免責(zé)聲明:我不是以下提到的來源的作者、所有者、投資者或貢獻者。我只是碰巧用它們來獲取一些資訊。