什么是XSS漏洞
試想一下,如果我們開發(fā)一個訂單系統(tǒng)分瘦,訂單名稱如果沒有做限制拇砰,允許用戶輸入任意字符,那么就有產(chǎn)生XSS的危險匕垫。攻擊者可以很容易編寫一個惡意JS腳本,然后將當(dāng)前登錄用戶的cookie或者其他敏感信息抓取到僧鲁,發(fā)送給攻擊者自己,這就是XSS(跨站腳本)攻擊。如何解決這個問題寞秃,首先我們想到的是在用戶輸入訂單的時候斟叼,我們對訂單名稱做限制,不允許輸入特殊字符春寿。這樣是可以避免的朗涩,不過對于一個大的系統(tǒng)來說,用戶可以輸入的字段太多了绑改,如果能夠全部校驗谢床,是最好的。如果不能做的話绢淀,還可以讓前端在做展示的時候進(jìn)行html轉(zhuǎn)義處理一下萤悴,這樣原本scirpt標(biāo)簽以及當(dāng)中的內(nèi)容就被當(dāng)做一個字符串展示出來瘾腰,而不是當(dāng)做代碼執(zhí)行了皆的。一般現(xiàn)在的reactjs等前端框架已經(jīng)默認(rèn)支持防xss了。今天我遇到的問題是蹋盆,有一部分freemarker寫的頁面存在XSS的問題费薄。
FreeMarker中解決XSS
可以通過對用戶輸入的字段進(jìn)行html轉(zhuǎn)義,來有效的避免XSS問題栖雾。freemarker模板中的變量通過{value?html}的形式未免工作量太大;查閱官方文檔析藕,通過escape標(biāo)簽可以對整個模板中$號引入的變量全部進(jìn)行一次html轉(zhuǎn)義召廷。比如:
<#assign x = "<test>">
<#macro m1>
m1: ${x}
</#macro>
<#escape x as x?html>
<#macro m2>m2: ${x}</#macro>
${x}
<@m1/>
</#escape>
${x}
<@m2/>
會輸出:
<test>
m1: <test>
<test>
m2: <test>
這種方式完全滿足現(xiàn)在的改造需求。在這個項目中账胧,freemarker模板是存儲在數(shù)據(jù)庫中竞慢,所有用到freemarker渲染的地方均是用的同一個入口;這樣工作量就不大了治泥。我們知道筹煮,freemarker加載模板是通過TemplateLoader這個接口來實現(xiàn)的;只需要在加載模板的時候在模板的頭部加上<#escape x as x?html>
在尾部加上</#escape
就可以對模板中所有的變量進(jìn)行html轉(zhuǎn)義了居夹。通過這種方式數(shù)據(jù)庫中的數(shù)據(jù)也不用修改败潦,將來生成模板,也不需要考慮做html轉(zhuǎn)義的問題准脂。
public interface TemplateLoader {
public Object findTemplateSource(String name)
throws IOException;
public long getLastModified(Object templateSource);
public Reader getReader(Object templateSource, String encoding) throws IOException;
public void closeTemplateSource(Object templateSource) throws IOException;
}
由于實現(xiàn)邏輯還是從外部讀取字符串加載劫扒,所以TemplateLoader接口支持html轉(zhuǎn)義的實現(xiàn)和StringTemplateLoader的邏輯差不多。
public class HtmlEscapeTemplateLoader implements TemplateLoader {
private static final String HTML_ESCAPE_PREFIX = "<#escape x as x?html>";
private static final String HTML_ESCAPE_SUFFIX = "</#escape>";
// 為了支持并發(fā)狸膏,這里采用ConcurrentMap
private final Map<String, StringTemplateSource> templates = Maps.newConcurrentMap();
public void putTemplate(String name, String templateContent) {
putTemplate(name, templateContent, System.currentTimeMillis());
}
public void putTemplate(String name, String templateContent, long lastModified) {
templates.put(name, new HtmlEscapeTemplateLoader.StringTemplateSource(name, templateContent, lastModified));
}
public boolean removeTemplate(String name) {
return templates.remove(name) != null;
}
public void closeTemplateSource(Object templateSource) {
}
public Object findTemplateSource(String name) {
return templates.get(name);
}
public long getLastModified(Object templateSource) {
return ((HtmlEscapeTemplateLoader.StringTemplateSource) templateSource).lastModified;
}
public Reader getReader(Object templateSource, String encoding) throws IOException {
Reader reader = new StringReader(((StringTemplateSource) templateSource).templateContent);
String templateText = IOUtils.toString(reader);
return new StringReader(HTML_ESCAPE_PREFIX + templateText + HTML_ESCAPE_SUFFIX);
}
private static class StringTemplateSource {
private final String name;
private final String templateContent;
private final long lastModified;
StringTemplateSource(String name, String templateContent, long lastModified) {
if (name == null) {
throw new IllegalArgumentException("name == null");
}
if (templateContent == null) {
throw new IllegalArgumentException("source == null");
}
if (lastModified < -1L) {
throw new IllegalArgumentException("lastModified < -1L");
}
this.name = name;
this.templateContent = templateContent;
this.lastModified = lastModified;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
HtmlEscapeTemplateLoader.StringTemplateSource other = (HtmlEscapeTemplateLoader.StringTemplateSource) obj;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
@Override
public String toString() {
return name;
}
}
/**
* Show class name and some details that are useful in template-not-found errors.
*
* @since 2.3.21
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClassNameForToString(this));
sb.append("(Map { ");
int cnt = 0;
for (String name : templates.keySet()) {
cnt++;
if (cnt != 1) {
sb.append(", ");
}
if (cnt > 10) {
sb.append("...");
break;
}
sb.append(StringUtil.jQuote(name));
sb.append("=...");
}
if (cnt != 0) {
sb.append(' ');
}
sb.append("})");
return sb.toString();
}
private static String getClassNameForToString(TemplateLoader templateLoader) {
final Class tlClass = templateLoader.getClass();
final Package tlPackage = tlClass.getPackage();
return tlPackage == Configuration.class.getPackage() || tlPackage == TemplateLoader.class.getPackage()
? tlClass.getSimpleName() : tlClass.getName();
}
}
至此實現(xiàn)了在freemarker中自動進(jìn)行html轉(zhuǎn)義避免XSS問題的過程沟饥。參考網(wǎng)上有些資料可以修改freemarker的源碼,對其在進(jìn)行$
號解析的時候,自動加上html轉(zhuǎn)義的邏輯闷板,也是可以的澎灸。如果某些特殊情況下,就是需要展示html形式的內(nèi)容而不需要轉(zhuǎn)義遮晚,或者有人錯誤的將變量進(jìn)行了一次轉(zhuǎn)義比如寫成${!(value)?html}
的形式性昭,會怎樣?
如果就是需要展示html形式的內(nèi)容而不需要轉(zhuǎn)義县遣,可以用<#noescape>
標(biāo)簽將不需要轉(zhuǎn)義的變量包裹起來糜颠,這樣就算外層有<#escape>
也不會進(jìn)行轉(zhuǎn)義了。如果在外部有<#escape>
的情況下萧求,變量自身又做了一次轉(zhuǎn)義其兴,那么該變量會被轉(zhuǎn)義兩次。正常字符不會有影響夸政,含有特殊字符的話元旬,可能比較難看了。
參考
https://blog.csdn.net/shadowsick/article/details/80768868
轉(zhuǎn)自: http://heqiao2010.github.io