昨天本博客受到了xss跨站腳本注入攻擊,3分鍾攻陷……其實攻擊者進攻的手法很簡單,沒啥技術含量。只能感嘆自己之前竟然完全沒防范。
這是數據庫里留下的一些記錄。最后那人弄了一個無限循環彈出框的腳本,估計這個腳本之后他再想輸入也沒法了。
類似這種:
<html> <body onload='while(true){alert(1)}'> </body> </html>
我立刻認識到這事件嚴重性,它說明我的博客有嚴重安全問題。因為xss跨站腳本攻擊可能導致用戶Cookie甚至服務器Session用戶信息被劫持,后果嚴重。雖然攻擊者就用些未必有什么技術含量的腳本即可做到。
第二天花些時間去了解,該怎么防范。順便也看了sql注入方面。
sql注入是源於sql語句的拼接。所以需要對用戶輸入參數化。由於我使用的是jpa,不存在sql拼接問題,但還是對一些用戶輸入做處理比較好。我的博客系統並不復雜,一共四個表,Article,User,Message,Comment。
涉及數據庫查詢且由用戶輸入的就只有用戶名,密碼,文章標題。其它后台產生的如文章日期一類就不用管。
對於這三個字段的校驗,可以使用自定義注解方式。
/**
* @ClassName: IsValidString
* @Description: 自定義注解實現前后台參數校驗,判斷是否包含非法字符
* @author 無名
* @date 2016-7-25 下午8:22:58
* @version 1.0
*/
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = IsValidString.ValidStringChecker.class) @Documented public @interface IsValidString { String message() default "The string is invalid."; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default{}; class ValidStringChecker implements ConstraintValidator<IsValidString,String> { @Override public void initialize(IsValidString arg0) { } @Override public boolean isValid(String strValue, ConstraintValidatorContext context) { //校驗方法添在這里 return true; } } }
定義了自定義注解以后就可以在對應的實體類字段上添上@IsValidString即可。
但由於我還沒研究出怎么攔截自定義注解校驗返回的異常,就在controller類里做校驗吧。
public static boolean contains_sqlinject_illegal_ch(String str_input) {
//"[`~!@#$%^&*()+=|{}':;',\\[\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]"
String regEx = "['=<>;\"]"; Pattern p = Pattern.compile(regEx); Matcher m = p.matcher(str_input); if (m.find()) { return true; } else { return false; } }
攔截的字符有 ' " [] <> ;
我覺得這幾個就夠了吧。<>順便就解決了xxs跨站腳本注入問題。
而xxs跨站腳本注入問題還是讓我很頭疼。因為我的博客系統使用wangEditor web文本編輯器,返回給后台的包含很多合法的html標簽,用來表現文章格式。所以不能統一過濾<>這類字符。
例如,將<html><body onload='while(true){alert(1)}'></body></html>這句輸入編輯器,提交。后台得到的是:
中間被轉意的<,>是合法的可供頁面顯示的<>字符。而外面的<p><br>就是文本編輯器生產的用來控制格式的正常的html標簽。
問題在於,如果有人點擊編輯器“源代碼”標識,將文本編輯器生產的正常的html標簽,再輸入這句<html><body onload='while(true){alert(1)}'></body></html>結果返回后台的就是原封不動的<html><body onload='while(true){alert(1)}'></body></html> <和>沒有變成<和>。
這讓人頭痛,我在想這個編輯器為什么提供什么狗屁查看源代碼功能,導致不能統一對<>。
在這種情況下,我只能過濾一部分認准是有危害的html標簽,而眾所周知,這類黑名單校驗是不夠安全的。(2016-12-30:下面這個函數是肯定不行的,寫的很蠢,下文已經把它干掉,用白名單校驗,並應用正則表達式的方式來做)
/*
* Cross-site scripting (XSS) is a type of computer security vulnerability
* typically found in web applications. XSS enables attackers to inject
* client-side scripts into web pages viewed by other users. A cross-site
* scripting vulnerability may be used by attackers to bypass access
* controls such as the same-origin policy. Cross-site scripting carried out
* on websites accounted for roughly 84% of all security vulnerabilities
* documented by Symantec as of 2007. Their effect may range from a petty
* nuisance to a significant security risk, depending on the sensitivity of
* the data handled by the vulnerable site and the nature of any security
* mitigation implemented by the site's owner.(From en.wikipedia.org)
*/
public static boolean contains_xss_illegal_str(String str_input) {
if (str_input.contains("<html") || str_input.contains("<HTML") || str_input.contains("<body") || str_input.contains("<BODY") || str_input.contains("<script") || str_input.contains("<SCRIPT") || str_input.contains("<link") || str_input.contains("<LINK") || str_input.contains("%3Cscript") || str_input.contains("%3Chtml") || str_input.contains("%3Cbody") || str_input.contains("%3Clink") || str_input.contains("%3CSCRIPT") || str_input.contains("%3CHTML") || str_input.contains("%3CBODY") || str_input.contains("%3CLINK") || str_input.contains("<META") || str_input.contains("<meta") || str_input.contains("%3Cmeta") || str_input.contains("%3CMETA") || str_input.contains("<style") || str_input.contains("<STYLE") || str_input.contains("%3CSTYLE") || str_input.contains("%3Cstyle") || str_input.contains("<xml") || str_input.contains("<XML") || str_input.contains("%3Cxml") || str_input.contains("%3CXML")) { return true; } else { return false; } }
我在考慮着把這個文本編輯器的查看源代碼功能給干掉。
另外,還是要系統學習xss跨站腳本注入防范。開始看一本書《白帽子講web安全》,覺得這本書不錯。
到時候有新見解再在這篇文章補充。
2016-12-30日補充:
今天讀了那本《白帽子講web安全》,果然獲益不少。其中提到富文本編輯器的情況,由於富文本編輯器本身會使用正常的一些html標簽,所以需要做白名單校驗。只允許使用一些確定安全的標簽,除富文本編輯器使用的標簽,其他的都過濾掉。這是白名單方式,是真正合理的。
另外下午研究下正則表達式的寫法:<([^(a)(img)(div)(p)(span)(pre)(br)(code)(b)(u)(i)(strike)(font)(blockquote)(ul)(li)(ol)(table)(tr)(td)(/)][^>]*)>(2016-12-30夜-2016-12-31 發現這個正則有誤,下面就繼續補充)
[^]是非的意思。
上面的正則的意思就是若含有a、img、div……之外的標簽則匹配。
/*
* Cross-site scripting (XSS) is a type of computer security vulnerability
* typically found in web applications. XSS enables attackers to inject
* client-side scripts into web pages viewed by other users. A cross-site
* scripting vulnerability may be used by attackers to bypass access
* controls such as the same-origin policy. Cross-site scripting carried out
* on websites accounted for roughly 84% of all security vulnerabilities
* documented by Symantec as of 2007. Their effect may range from a petty
* nuisance to a significant security risk, depending on the sensitivity of
* the data handled by the vulnerable site and the nature of any security
* mitigation implemented by the site's owner.(From en.wikipedia.org)
*/
public static boolean contains_xss_illegal_str(String str_input) {
final String REGULAR_EXPRESSION =
"<([^(a)(img)(div)(p)(span)(pre)(br)(code)(b)(u)(i)(strike)(font)(blockquote)(ul)(li)(ol)(table)(tr)(td)(/)][^>]*)>"; Pattern pattern = Pattern.compile(REGULAR_EXPRESSION); Matcher matcher = pattern.matcher(str_input); if (matcher.find()) { return true; } else { return false; } }
2016-12-30夜-2016-12-31 補充:
實驗發現前面寫的那個正則表達式是無效的。同時發現這個正則是非常難寫、很有技術含量的,對於我這個基本正則都不太熟悉的菜鳥來說。
這種‘非’的表達,不能簡單的用上面提到的[^]。那種無法匹配字符串的非。例如(a[^bc]d)表示地是ad其中的字符串不能為b或c。
對於字符串的非,應該用這種表達式:^(?!.*helloworld).*$
以此為前提,下面的正則可以表達不為<p>的html標簽:
<((?!p)[^>])> 后面[^]表示<>中只有一個字符(?!p)且第一個字符非p
若寫成<((?!p)[^>]*)>則表示有n個字符,且第一個字符非p
@Test
public void test_Xss_check() { System.out.println("begin"); String str_input = "<p>"; final String REGULAR_EXPRESSION = "<((?!p)[^>])>"; Pattern pattern = Pattern.compile(REGULAR_EXPRESSION); Matcher matcher = pattern.matcher(str_input); if (matcher.find()) { System.out.println("yes"); } }
那么該如何匹配,不為AA且不為BB的html標簽呢?
<((?!p)(?!a)[^>]*)>匹配的就是不以p開頭且不以a開頭html標簽!
我們要求的匹配的是:不為<b>、不為<ul>、不為<li>……且不以<a 開頭、不以<img 開頭、不以</開頭……的html標簽。該如何寫?
先寫一個簡單的例子:<(((?!p )(?!a )[^>]*)((?!p)(?!a).))>匹配的是非<p xxxx>且非<a xxxx>且非<p>且非<a>的<html>標簽。
例如,字符串<pasd>則匹配,<p asd>則不匹配,<p>則不匹配。然而不精准的一點是,<ppp>或<aaa>也不匹配。其他問題也有,例如非<table>的標簽就不知道該怎么表示。
總之感覺這個正則很難寫,超出了我的能力范圍。所以最后決定用正則先篩選html標簽,再由java代碼做白名單篩選。
用於篩選html標簽的正則是<(?!a )(?!p )(?!img )(?!code )(?!spab )(?!pre )(?!font )(?!/)[^>]*>,篩選到的html排除掉<a xxx><p xxx><img xx></>等等,因為那些是默認合法的。篩選得到的<html>標簽存進List里,再做白名單校驗。
代碼如下:
@Test public void test_Xss_check() { String str_input = "<a ss><script>sds<body><a></adsd><d/s><p dsd><pp><a><dsds>dsdas<font ds>" + "<fontdsdsd><font>das<oooioacc><pp sds><script><code ><br><code><ccc><abug>"; System.out.println("String inputed:" + str_input); final String REGULAR_EXPRESSION = "<(?!a )(?!p )(?!img )(?!code )(?!spab )(?!pre )(?!font )(?!/)[^>]*>"; final Pattern PATTERN = Pattern.compile(REGULAR_EXPRESSION); final Matcher MATCHER = PATTERN.matcher(str_input); List<String> str_lst = new ArrayList<String>(); while (MATCHER.find()) { str_lst.add(MATCHER.group()); } final String LEGAL_TAGS = "<a><img><div><p><span><pre><br><code>" + "<b><u><i><strike><font><blockquote><ul><li><ol><table><tr><td>"; for (String str:str_lst) { if (!LEGAL_TAGS.contains(str)) { System.out.println(str + " is illegal"); } } }
上述代碼輸出為:
String inputed:<a ss><script>sds<body><a></adsd><d/s><p dsd><pp><a><dsds>dsdas<font ds><fontdsdsd><font>das<oooioacc><pp sds><script><code ><br><code><ccc><abug>
<script> is illegal
<body> is illegal
<d/s> is illegal
<pp> is illegal
<dsds> is illegal
<fontdsdsd> is illegal
<oooioacc> is illegal
<pp sds> is illegal
<script> is illegal
<ccc> is illegal
<abug> is illegal
2017年1月1日
新年好,然而,不得不再說下這個xss白名單校驗的新進展。昨天,更新了上述校驗方法。那個腳本小子又來了,根據上文內容可知,我現在做到的是只限定有限的html標簽,但沒對標簽屬性做限制。結果這個腳本小子就拿這個做文章。比如把p標簽設為絕對定位,綁定指定位置,設置長寬,一類的……
而且onclick、onload這些東西很多標簽都有。
所以上文所述的方法寫的也不夠。但又感覺去再校驗屬性對我來說好麻煩。就上網上找找別人怎么做的。最后就找到了jsoup這個開源jar包。
https://jsoup.org/download
引入jar包后,這樣寫即可:
articleContent = Jsoup.clean(articleContent, Whitelist.basicWithImages());
媽的,能用輪子就盡快用,自己造太難了,浪費我五天。
最后,祝天下所有腳本猴子,2017年倒大霉!!!
2017年1月10日
上次加了Jsoup的過濾后,感覺寫博客方面有些問題。明顯是一些不該被過濾的標簽被過濾掉了。
articleContent = Jsoup.clean(articleContent,Whitelist.basicWithImages());
覺得有必要繼續處理。
設置斷點調試。作為例子,博客中寫這樣的html代碼:
<html> <body> <audio controls="controls" autoplay="autoplay" height="100" width="100"> <source src="<%=basePath %>music/Breath and Life.mp3" type="audio/mp3" /> <source src="<%=basePath %>music/Breath and Life.ogg" type="audio/ogg" /> <embed height="100" width="100" src="<%=basePath %>music/Breath and Life.mp3" /> </audio> <script type="text/javascript" src="<%=basePath %>js/global.js"></script> <script type="text/javascript" src="<%=basePath %>js/photos.js"></script> </body> </html>
富文本編輯器傳到后台的字符串為:
<p>hello,日向blog</p><pre style="max-width:100%;overflow-x:auto;"><code class="html hljs xml"
codemark="1"><span class="hljs-tag"><<span class="hljs-name">html</span>></span>
<span class="hljs-tag"><<span class="hljs-name">body</span>></span>
<span class="hljs-tag"><<span class="hljs-name">audio</span> <span class="hljs-attr">controls</span>=<span class="hljs-
string">"controls"</span> <span class="hljs-attr">autoplay</span>=<span class="hljs-string">"autoplay"</span> <span class="hljs-
attr">height</span>=<span class="hljs-string">"100"</span> <span class="hljs-attr">width</span>=<span class="hljs-
string">"100"</span>></span>
<span class="hljs-tag"><<span class="hljs-name">source</span> <span class="hljs-attr">src</span>=<span class="hljs-
string">"<%=basePath %>music/Breath and Life.mp3"</span> <span class="hljs-attr">type</span>=<span class="hljs-
string">"audio/mp3"</span> /></span>
<span class="hljs-tag"><<span class="hljs-name">source</span> <span class="hljs-attr">src</span>=<span class="hljs-
string">"<%=basePath %>music/Breath and Life.ogg"</span> <span class="hljs-attr">type</span>=<span class="hljs-
string">"audio/ogg"</span> /></span>
<span class="hljs-tag"><<span class="hljs-name">embed</span> <span class="hljs-attr">height</span>=<span class="hljs-
string">"100"</span> <span class="hljs-attr">width</span>=<span class="hljs-string">"100"</span> <span class="hljs-attr">src</span>=<span
class="hljs-string">"<%=basePath %>music/Breath and Life.mp3"</span> /></span>
<span class="hljs-tag"></<span class="hljs-name">audio</span>></span>
<span class="hljs-tag"><<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-
string">"text/javascript"</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"<%=basePath
%>js/global.js"</span>></span><span class="undefined"></span><span class="hljs-tag"></<span class="hljs-
name">script</span>></span>
<span class="hljs-tag"><<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-
string">"text/javascript"</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"<%=basePath
%>js/photos.js"</span>></span><span class="undefined"></span><span class="hljs-tag"></<span class="hljs-
name">script</span>></span>
<span class="hljs-tag"></<span class="hljs-name">body</span>></span>
<span class="hljs-tag"></<span class="hljs-name">html</span>></span></code></pre>
<p><br></p>
經jsoup過濾后的值為:
<p>hello,日向blog</p>
<pre><code><span><<span>html</span>></span>
<span><<span>body</span>></span>
<span><<span>audio</span> <span>controls</span>=<span>"controls"</span> <span>autoplay</span>=<span>"autoplay"</span> <span>height</span>=<span>"100"</span> <span>width</span>=<span>"100"</span>></span>
<span><<span>source</span> <span>src</span>=<span>"<%=basePath %>music/Breath and Life.mp3"</span> <span>type</span>=<span>"audio/mp3"</span> /></span>
<span><<span>source</span> <span>src</span>=<span>"<%=basePath %>music/Breath and Life.ogg"</span> <span>type</span>=<span>"audio/ogg"</span> /></span>
<span><<span>embed</span> <span>height</span>=<span>"100"</span> <span>width</span>=<span>"100"</span> <span>src</span>=<span>"<%=basePath %>music/Breath and Life.mp3"</span> /></span>
<span></<span>audio</span>></span>
<span><<span>script</span> <span>type</span>=<span>"text/javascript"</span> <span>src</span>=<span>"<%=basePath %>js/global.js"</span>></span><span></span><span></<span>script</span>></span>
<span><<span>script</span> <span>type</span>=<span>"text/javascript"</span> <span>src</span>=<span>"<%=basePath %>js/photos.js"</span>></span><span></span><span></<span>script</span>></span>
<span></<span>body</span>></span>
<span></<span>html</span>></span></code></pre>
<p><br></p>
顯然pre標簽的style、span和code標簽的class屬性被過濾掉了,而這些屬性是無害而必須的。所以,我們需要改動jsoup原有的白名單。
查看代碼,了解到Jsoup的過濾是通過傳入Whitelist.basicWithImages()這個參數實現的,這是個白名單。
查看其源代碼:
/** <p> This whitelist allows a fuller range of text nodes: <code>a, b, blockquote, br, cite, code, dd, dl, dt, em, i, li, ol, p, pre, q, small, span, strike, strong, sub, sup, u, ul</code>, and appropriate attributes. </p> <p> Links (<code>a</code> elements) can point to <code>http, https, ftp, mailto</code>, and have an enforced <code>rel=nofollow</code> attribute. </p> <p> Does not allow images. </p> @return whitelist */ public static Whitelist basic() { return new Whitelist() .addTags( "a", "b", "blockquote", "br", "cite", "code", "dd", "dl", "dt", "em", "i", "li", "ol", "p", "pre", "q", "small", "span", "strike", "strong", "sub", "sup", "u", "ul") .addAttributes("a", "href") .addAttributes("blockquote", "cite") .addAttributes("q", "cite") .addProtocols("a", "href", "ftp", "http", "https", "mailto") .addProtocols("blockquote", "cite", "http", "https") .addProtocols("cite", "cite", "http", "https") .addEnforcedAttribute("a", "rel", "nofollow") ; } /** This whitelist allows the same text tags as {@link #basic}, and also allows <code>img</code> tags, with appropriate attributes, with <code>src</code> pointing to <code>http</code> or <code>https</code>. @return whitelist */ public static Whitelist basicWithImages() { return basic() .addTags("img") .addAttributes("img", "align", "alt", "height", "src", "title", "width") .addProtocols("img", "src", "http", "https") ; }
我做了修改后為:
public static Whitelist basic() { return new Whitelist() .addTags( "a", "b", "blockquote", "br", "cite", "code", "dd", "dl", "dt", "em", "i", "li", "ol", "p", "pre", "q", "small", "span", "strike", "strong", "sub", "sup", "u", "ul") .addAttributes("a", "href") .addAttributes("blockquote", "cite") .addAttributes("q", "cite") .addAttributes("code", "class") .addAttributes("span", "class") .addAttributes("pre", "style") .addProtocols("a", "href", "ftp", "http", "https", "mailto") .addProtocols("blockquote", "cite", "http", "https") .addProtocols("cite", "cite", "http", "https") .addEnforcedAttribute("a", "rel", "nofollow") ; }
添加了:
.addAttributes("span", "class")
.addAttributes("pre", "style")
.addAttributes("code", "class")
這三項