cleaner是Jsoup的重要功能之一,咱們經常使用它來進行富文本輸入中的XSS防護。javascript
咱們知道,XSS攻擊的通常方式是,經過在頁面輸入中嵌入一段惡意腳本,對輸出時的DOM結構進行修改,從而達到執行這段腳本的目的。對於純文本輸入,過濾/轉義HTML特殊字符<
,>
,"
,'
是行之有效的辦法,可是若是自己用戶輸入的就是一段HTML文本(例如博客文章),這種方式就不太有效了。這個時候,就是Jsoup大顯身手的時候了。java
在前面,咱們已經知道了,Jsoup裏怎麼將HTML變成一棵DOM樹,怎麼對DOM樹進行遍歷,怎麼對DOM文檔進行輸出,那麼其實cleaner的實現方式,也能猜出大概了。使用Jsoup進行XSS防護,大體分爲三個步驟:node
將HTML解析爲DOM樹面試
這一步能夠過濾掉一些企圖搞破壞的非閉合標籤、非正常語法等。例如一些輸入,會嘗試用</textarea>
閉合當前Tag,而後寫入攻擊腳本。而根據前面對Jsoup的parser的分析,這種時候,這些非閉合標籤會被當作錯誤並丟棄。app
過濾高風險標籤/屬性/屬性值xss
高風險標籤是指<script>
以及相似標籤,對屬性/屬性值進行過濾是由於某些屬性值裏也能夠寫入javascript腳本,例如onclick='alert("xss!")'
。gradle
從新將DOM樹輸出爲HTML文本ui
DOM樹的輸出,在前面(Jsoup代碼解讀之三)已經提到過了。this
對於上述的兩個步驟,一、3都已經分別在parser和輸出中完成,如今只剩下步驟 2:過濾高風險標籤等。spa
Jsoup給出的答案是白名單。下面是Whitelist
的部分代碼。
public class Whitelist { private Set<TagName> tagNames; // tags allowed, lower case. e.g. [p, br, span] private Map<TagName, Set<AttributeKey>> attributes; // tag -> attribute[]. allowed attributes [href] for a tag. private Map<TagName, Map<AttributeKey, AttributeValue>> enforcedAttributes; // always set these attribute values private Map<TagName, Map<AttributeKey, Set<Protocol>>> protocols; // allowed URL protocols for attributes private boolean preserveRelativeLinks; // option to preserve relative links }
這裏定義了標籤名/屬性名/屬性值的白名單。
而Cleaner
是過濾的執行者。不出所料,Cleaner內部定義了CleaningVisitor
來進行標籤的過濾。CleaningVisitor的過濾過程並不改變原始DOM樹的值,而是將符合條件的屬性,加入到Element destination
裏去。
private final class CleaningVisitor implements NodeVisitor { private int numDiscarded = 0; private final Element root; private Element destination; // current element to append nodes to private CleaningVisitor(Element root, Element destination) { this.root = root; this.destination = destination; } public void head(Node source, int depth) { if (source instanceof Element) { Element sourceEl = (Element) source; if (whitelist.isSafeTag(sourceEl.tagName())) { // safe, clone and copy safe attrs ElementMeta meta = createSafeElement(sourceEl); Element destChild = meta.el; destination.appendChild(destChild); numDiscarded += meta.numAttribsDiscarded; destination = destChild; } else if (source != root) { // not a safe tag, so don't add. don't count root against discarded. numDiscarded++; } } else if (source instanceof TextNode) { TextNode sourceText = (TextNode) source; TextNode destText = new TextNode(sourceText.getWholeText(), source.baseUri()); destination.appendChild(destText); } else { // else, we don't care about comments, xml proc instructions, etc numDiscarded++; } } public void tail(Node source, int depth) { if (source instanceof Element && whitelist.isSafeTag(source.nodeName())) { destination = destination.parent(); // would have descended, so pop destination stack } } }
至此,Jsoup的所有模塊都已經寫完了。Jsoup源碼並很少,只有14000多行,可是實現很是精巧,在讀代碼的過程當中,除了相關知識,還驗證幾個很重要的思想:
最好的代碼抽象,是對現實概念的映射。
這句話在看《代碼大全》的時候印象很深入。在Jsoup裏,只要有相關知識,每一個類的做用都能第一時間明白其做用。
不要過分抽象
在Jsoup裏,只用到了兩個接口,一個是NodeVisitor
,一個是Connection
,其餘都是用抽象類或者直接用實現類代替。記得有次面試的時候被問到咱們開發中每逢一個功能,都要先定義一個接口的作法是否必要?如今的答案是沒有必要,過分的抽象反而會下降代碼質量。
另外,Jsoup的代碼內聚性都很高,每一個類的功能基本都定義在類的內部,這是一個典型的充血模型。同時有大量的facade使用,而避免了Factory、Configure等類的出現,我的感受這點是很是好的。