Jsoup代碼解讀之八-防御XSS攻擊
來自: http://www.importnew.com/17893.html
防御XSS攻擊的一般原理
cleaner是Jsoup的重要功能之一,我們常用它來進行富文本輸入中的XSS防御。
我們知道,XSS攻擊的一般方式是,通過在頁面輸入中嵌入一段惡意腳本,對輸出時的DOM結構進行修改,從而達到執行這段腳本的目的。對于純文本輸入,過濾/轉義HTML特殊字符<,>,”,’是行之有效的辦法,但是如果本身用戶輸入的就是一段HTML文本(例如博客文章),這種方式就不太有效了。這個時候,就是Jsoup大顯身手的時候了。
在前面,我們已經知道了,Jsoup里怎么將HTML變成一棵DOM樹,怎么對DOM樹進行遍歷,怎么對DOM文檔進行輸出,那么其實cleaner的實現方式,也能猜出大概了。使用Jsoup進行XSS防御,大致分為三個步驟:
- 將HTML解析為DOM樹
這一步可以過濾掉一些企圖搞破壞的非閉合標簽、非正常語法等。例如一些輸入,會嘗試用</textarea>閉合當前Tag,然后寫入攻擊腳本。而根據前面對Jsoup的parser的分析,這種時候,這些非閉合標簽會被當做錯誤并丟棄。
- 過濾高風險標簽/屬性/屬性值
高風險標簽是指<script>以及類似標簽,對屬性/屬性值進行過濾是因為某些屬性值里也可以寫入javascript腳本,例如onclick=’alert(“xss!”)’。
- 重新將DOM樹輸出為HTML文本
DOM樹的輸出,在前面(Jsoup代碼解讀之三)已經提到過了。
Cleaner與Whitelist
對于上述的兩個步驟,1、3都已經分別在parser和輸出中完成,現在只剩下步驟 2:過濾高風險標簽等。
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等類的出現,個人感覺這點是非常好的。
最后繼續貼上Jsoup解讀系列的github地址:https://github.com/code4craft/jsoup-learning/