最近接手之前同僚的幾個項目,公司利用掃描工具進行全項目掃描,發現了部分項目代碼存在安全漏洞,是以需要進行項目代碼修複以避免有人惡意攻擊。這個任務自然而然的就落到我手上。在這裡記錄一下操作的過程。
掃描出來的漏洞主要有兩種,一種是SQL注入,一種是XSS攻擊。以下就是我的一個解決過程。
SQL注入
什麼是SQL注入,百度百科這樣定義的:SQL注入是将Web頁面的原URL、表單域或資料包輸入的參數,修改拼接成SQL語句,傳遞給Web伺服器,進而傳給資料庫伺服器以執行資料庫指令。如Web應用程式的開發人員對使用者所輸入的資料或cookie等内容不進行過濾或驗證(即存在注入點)就直接傳輸給資料庫,就可能導緻拼接的SQL被執行,擷取對資料庫的資訊以及提權,發生SQL注入攻擊。可能有的人還不太了解,這裡做一個簡單的例子以幫助了解。
// SQL注入
String param = "'kmli' or 1=1";
String sql = "select student from stu where name = " + param; // 拼接SQL參數
PreparedStatement preparedStatement = connection.prepareStatement(sql);
ResultSet resultSet = preparedStatement.executeQuery();
System.out.println(resultSet.next());
輸出結果為true,DB中執行的SQL為:
// 永真條件1=1成為了查詢條件的一部分,可以傳回所有資料,造成了SQL注入問題
select student from stu where name = 'kmli' or 1=1
以上就是一個簡單的SQL注入的例子。攻擊者隻要在查詢的字段上面添加相關的惡意執行腳本就可以做到SQL注入,進而擷取相關的資訊。檢視我們項目的SQL語句,發現也是采用類似的方式進行查詢:
sql.append(" and a.ResourceId=").append(resourceId);
sql.append(" and a.PeopleProperty like '").append("%").append(resourceName).append("%' ");
是以需要對sql語句進行改進,不可以使用這種直接的方式進行擷取。PreparedStatement類為我們提供了解決方案,我們可以使用以下方式防止SQL注入:
// 防止SQL注入
String param = "'kmli' or 1=1";
String sql = "select student from stu where name = " + param;
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, param);
ResultSet resultSet = preparedStatement.executeQuery();
System.out.println(resultSet.next());
此時的輸出結果為false,DB中執行的SQL為:
// 永真條件1=1成為了查詢條件的一部分,可以傳回所有資料,造成了SQL注入問題
select student from stu where name = '\'kmli\' or 1=1'
我們可以看到輸出的SQL文是把整個參數用引号包起來,并把參數中的引号作為轉義字元,進而避免了參數也作為條件的一部分。
PreparedStatement就是為了提高statement(包括SQL,存儲過程等)執行的效率,那麼PreparedStatement為什麼可以防止SQL注入呢?打開java.sql.PreparedStatement通用接口,看到如下注釋:
An object that represents a precompiled SQL statement.
A SQL statement is precompiled and stored in a PreparedStatement object.
This object can then be used to efficiently execute this statement multiple times.
我們知道,SQL執行過程分為以下幾個步驟:
1.Convert given SQL query into DB format -- 将SQL語句轉化為DB形式(文法樹結構)
2.Check for syntax -- 檢查文法
3.Check for semantics -- 檢查語義
4.Prepare execution plan -- 準備執行計劃
5.Set the run-time values into the query -- 設定運作時的參數
6.Run the query and fetch the output -- 執行查詢并取得結果
在上面的解釋中看到A SQL statement is precompiled,那麼什麼是“precompiled SQL statement”呢?“precompiled SQL statement”,就是同樣的SQL文(包括不同參數的),1-4步驟隻在第一次執行,是以大大提高了執行效率(特别是對于需要重複執行同一SQL的)。接下來在具體看看PreparedStatement接口的setString方法(其它設定參數的方法諸如setInt,setDouble之類,編譯器會檢查參數類型,已經避免了SQL注入),以下為部分代碼:
public void setString(int parameterIndex, String x) throws SQLException {
synchronized (checkClosed().getConnectionMutex()) {
// if the passed string is null, then set this column to null
if (x == null) {
setNull(parameterIndex, Types.CHAR);
} else {
checkClosed();
int stringLength = x.length();
if (this.connection.isNoBackslashEscapesSet()) {
// Scan for any nasty chars
// 判斷是否需要轉義處理(比如包含引号,換行等字元)
boolean needsHexEscape = isEscapeNeededForString(x, stringLength);
// 如果不需要轉義,則在兩邊加上單引号
if (!needsHexEscape) {
byte[] parameterAsBytes = null;
StringBuilder quotedString = new StringBuilder(x.length() + 2);
quotedString.append('\'');
quotedString.append(x);
quotedString.append('\'');
...
} else {
...
}
String parameterAsString = x;
boolean needsQuoted = true;
// 如果需要轉義,則做轉義處理
if (this.isLoadDataQuery || isEscapeNeededForString(x, stringLength)) {
...
從以上注釋的部分明白為什麼參數會被單引号包裹,并且類似單引号之類的特殊字元會被轉義處理,而就是因為這些才避免了SQL注入。 是以最後我也将代碼中使用SQL的備份進行了修改:
sql.append(" and a.ResourceId = ?");
parameters.set(index++,resourceId);
sql.append(" and a.PeopleProperty like concat('%',?,'%') ");
parameters.set(index++, resourceName);
大家看不懂上面的代碼不要緊,最重要的實作原理是一樣的,我這樣寫隻是因為采用了公式的指派方式,至此,SQL注入的問題得到了解決。
XSS攻擊
XSS,跨站腳本攻擊,人們經常将跨站腳本攻擊(Cross Site Scripting)縮寫為CSS,但這會與層疊樣式表(Cascading Style Sheets,CSS)的縮寫混淆。是以,有人将跨站腳本攻擊縮寫為XSS。百度百科這樣解釋的:XSS攻擊通常指的是通過利用網頁開發時留下的漏洞,通過巧妙的方法注入惡意指令代碼到網頁,使使用者加載并執行攻擊者惡意制造的網頁程式。這些惡意網頁程式通常是JavaScript,但實際上也可以包括Java、 VBScript、ActiveX、 Flash 或者甚至是普通的HTML。攻擊成功後,攻擊者可能得到包括但不限于更高的權限(如執行一些操作)、私密網頁内容、會話和cookie等各種内容。聽起來和SQL有類似的地方。這裡不做進一步的展開,大家不太明白的可以自行百度。
常見的XSS的攻擊方式有以下幾種:
1、盜用cookie,擷取敏感資訊。
2、利用植入Flash,通過crossdomain權限設定進一步擷取更高權限;或者利用Java等得到類似的操作。
3、利用iframe、frame、XMLHttpRequest或上述Flash等方式,以(被攻擊)使用者的身份執行一些管理動作,或執行一些一般的如發微網誌、加好友、發私信等操作。
4、利用可被攻擊的域受到其他域信任的特點,以受信任來源的身份請求一些平時不允許的操作,如進行不當的投票活動。
5、在通路量極大的一些頁面上的XSS可以攻擊一些小型網站,實作DDoS攻擊的效果。
要想對XSS攻擊進行防禦,注意可以考慮以下幾點:
(1)基于特征的防禦
XSS漏洞和著名的SQL注入漏洞一樣,都是利用了Web頁面的編寫不完善,是以每一個漏洞所利用和針對的弱點都不盡相同。這就給XSS漏洞防禦帶來了困難,不可能以單一特征來概括所有XSS攻擊。傳統的XSS防禦在進行攻擊鑒别時多采用特征比對方式,主要是針對“javascript”這個關鍵字進行檢索,但是這種鑒别不夠靈活,凡是送出的資訊中各有“javascript”時,就被硬性的被判定為XSS攻擊。
(2)基于代碼修改的防禦
Web頁面開發者在編寫程式時往往會出現一些失誤和漏洞,XSS攻擊正是利用了失誤和漏洞,是以一種比較理想的方法就是通過優化Web應用開發來減少漏洞,避免被攻擊:1)使用者向伺服器上送出的資訊要對URL和附帶的的HTTP頭、POST資料等進行查詢,對不是規定格式、長度的内容進行過濾。2)實作Session标記(session tokens)、CAPTCHA系統或者HTTP引用頭檢查,以防功能被第三方網站所執行。3)确認接收的的内容被妥善的規範化,僅包含最小的、安全的Tag(沒有javascript),去掉任何對遠端内容的引用(尤其是樣式表和javascript),使用HTTP only的cookie。
(3)用戶端分層防禦政策
用戶端跨站腳本攻擊的分層防禦政策是基于獨立配置設定線程和分層防禦政策的安全模型。它建立在用戶端(浏覽器),這是它與其他模型最大的差別,之是以用戶端安全性如此重要,用戶端在接受伺服器資訊,選擇性的執行相關内容。這樣就可以使防禦XSS攻擊變得容易,該模型主要由三大部分組成:
1)對每一個網頁配置設定獨立線程且分析資源消耗的“網頁線程分析子產品”;
2)包含分層防禦政策四個規則的使用者輸入分析子產品;
3)儲存網際網路上有關XSS惡意網站資訊的XSS資訊資料庫。
XSS攻擊主要是由程式漏洞造成的,要完全防止XSS安全漏洞主要依靠程式員較高的程式設計能力和安全意識,當然安全的軟體開發流程及其他一些程式設計安全原則也可以大大減少XSS安全漏洞的發生。這些防範XSS漏洞原則包括:
(1)不信任使用者送出的任何内容,對所有使用者送出内容進行可靠的輸入驗證,包括對URL、查詢關鍵字、HTTP頭、REFER、POST資料等,僅接受指定長度範圍内、采用适當格式、采用所預期的字元的内容送出,對其他的一律過濾。盡量采用POST而非GET送出表單;對“<”,“>”,“;”,“””等字元做過濾;任何内容輸出到頁面之前都必須加以en-code,避免不小心把htmltag顯示出來。
(2)實作Session 标記(session tokens)、CAPTCHA(驗證碼)系統或者HTTP引用頭檢查,以防功能被第三方網站所執行,對于使用者送出資訊的中的img等link,檢查是否有重定向回本站、不是真的圖檔等可疑操作。
(3)cookie 防盜。避免直接在cookie中洩露使用者隐私,例如email、密碼,等等;通過使cookie和系統IP綁定來降低cookie洩露後的危險。這樣攻擊者得到的cookie沒有實際價值,很難拿來直接進行重播攻擊。
(4)确認接收的内容被妥善地規範化,僅包含最小的、安全的Tag(沒有JavaScript),去掉任何對遠端内容的引用(尤其是樣式表和JavaScript),使用HTTPonly的cookie。
基于以上幾點,由于項目比較老,采用的是SSM架構,是以第一步,需要在web.xml檔案中加入過濾器(大家依據自己的需要改成自己過濾器的名稱):
<!-- xss過濾器 -->
<filter>
<filter-name>XssFilter</filter-name>
<filter-class>ttd.tvm.manage.common.XssFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>XssFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
第二步,在相應的包下建立對應的過濾器類:
/**
* @Author likangmin
* @create 2020/11/23 13:39
*/
public class XssFilter implements Filter {
FilterConfig filterConfig = null;
public void init(FilterConfig filterConfig) throws ServletException {
this.filterConfig = filterConfig;
}
public void destroy() {
this.filterConfig = null;
}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
chain.doFilter(new XssHttpServletRequestWrapperFilter(
(HttpServletRequest) request), response);
}
}
然後寫自定義過濾器:
/**
* @Author likangmin
* @create 2020/11/23 13:41
*/
public class XssHttpServletRequestWrapperFilter extends HttpServletRequestWrapper {
HttpServletRequest orgRequest = null;
public XssHttpServletRequestWrapperFilter(HttpServletRequest request) {
super(request);
orgRequest = request;
}
/**
* 覆寫getParameter方法,将參數名和參數值都做xss過濾。
* 如果需要獲得原始的值,則通過super.getParameterValues(name)來擷取
* getParameterNames,getParameterValues和getParameterMap也可能需要覆寫
*/
@Override
public String getParameter(String name) {
String value = super.getParameter(xssEncode(name));
if (value != null) {
value = xssEncode(value);
value = HTMLEncode(value);
}
return value;
}
/**
* spring是使用的getParameterValues方法
*/
@Override
public String[] getParameterValues(String name) {
String[] value = super.getParameterValues(name);
if (value == null) {
return null;
}
for (int i = 0;i <value.length;i++) {
value[i] = xssEncode(value[i]);
value[i] = HTMLEncode(value[i]);
}
return value;
}
/**
* 對一些特殊字元進行轉義
*
*
*/
public static String HTMLEncode(String aText) {
final StringBuilder result = new StringBuilder();
final StringCharacterIterator iterator = new StringCharacterIterator(aText);
char character = iterator.current();
while (character != CharacterIterator.DONE) {
if (character == '<') {
result.append("<");
} else if (character == '>') {
result.append(">");
} else if (character == '\"') {
result.append("`");
} else {
result.append(character);
}
character = iterator.next();
}
return result.toString();
}
/**
* 覆寫getHeader方法,将參數名和參數值都做xss過濾。 如果需要獲得原始的值,則通過super.getHeaders(name)來擷取
* getHeaderNames 也可能需要覆寫
*/
@Override
public String getHeader(String name) {
String value = super.getHeader(xssEncode(name));
if (value != null) {
value = xssEncode(value);
}
return value;
}
/**
* 将容易引起xss漏洞的半角字元直接替換成全角字元
* 目前xssProject對注入代碼要求是必須開始标簽和結束标簽(如<script></script>)正确比對才能解析,否則報錯;是以隻能替換調xssProject換為自定義實作
*
* @param s
* @return
*/
private static String xssEncode(String s) {
if (s == null || s.isEmpty()) {
return s;
}
String result = stripXSS(s);
if (null != result) {
result = escape(result);
}
return result;
}
public static String escape(String s) {
StringBuilder sb = new StringBuilder(s.length() + 16);
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
switch (c) {
case '>':
sb.append('>');// 全角大于号
break;
case '<':
sb.append('<');// 全角小于号
break;
case '\'':
sb.append('‘');// 全角單引号
break;
case '\"':
sb.append('“');// 全角雙引号
break;
case '\\':
sb.append('\');// 全角斜線
break;
case '%':
sb.append('%'); // 全角冒号
break;
default:
sb.append(c);
break;
}
}
return sb.toString();
}
private static String stripXSS(String value) {
if (value != null) {
// Avoid null characters
value = value.replaceAll("", "");
// Avoid anything between script tags
Pattern scriptPattern = Pattern.compile("<script>(.*?)</script>", Pattern.CASE_INSENSITIVE);
value = scriptPattern.matcher(value).replaceAll("");
// Avoid anything in a src='...' type of expression
scriptPattern = Pattern.compile("src[\r\n]*=[\r\n]*\\\'(.*?)\\\'",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
value = scriptPattern.matcher(value).replaceAll("");
scriptPattern = Pattern.compile("src[\r\n]*=[\r\n]*\\\"(.*?)\\\"",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
value = scriptPattern.matcher(value).replaceAll("");
// Remove any lonesome </script> tag
scriptPattern = Pattern.compile("</script>", Pattern.CASE_INSENSITIVE);
value = scriptPattern.matcher(value).replaceAll("");
// Remove any lonesome <script ...> tag
scriptPattern = Pattern.compile("<script(.*?)>",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
value = scriptPattern.matcher(value).replaceAll("");
// Avoid eval(...) expressions
scriptPattern = Pattern.compile("eval\\((.*?)\\)",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
value = scriptPattern.matcher(value).replaceAll("");
// Avoid expression(...) expressions
scriptPattern = Pattern.compile("expression\\((.*?)\\)",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
value = scriptPattern.matcher(value).replaceAll("");
// Avoid javascript:... expressions
scriptPattern = Pattern.compile("javascript:", Pattern.CASE_INSENSITIVE);
value = scriptPattern.matcher(value).replaceAll("");
value = scriptPattern.matcher(value).replaceAll("");
// Avoid οnlοad= expressions
scriptPattern = Pattern.compile("onload(.*?)=",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
value = scriptPattern.matcher(value).replaceAll("");
scriptPattern = Pattern.compile("<iframe>(.*?)</iframe>", Pattern.CASE_INSENSITIVE);
value = scriptPattern.matcher(value).replaceAll("");
scriptPattern = Pattern.compile("</iframe>", Pattern.CASE_INSENSITIVE);
value = scriptPattern.matcher(value).replaceAll("");
// Remove any lonesome <script ...> tag
scriptPattern = Pattern.compile("<iframe(.*?)>",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
value = scriptPattern.matcher(value).replaceAll("");
}
return value;
}
/**
* 擷取最原始的request
*
* @return
*/
public HttpServletRequest getOrgRequest() {
return orgRequest;
}
/**
* 擷取最原始的request的靜态方法
*
* @return
*/
public static HttpServletRequest getOrgRequest(HttpServletRequest req) {
if (req instanceof XssHttpServletRequestWrapperFilter) {
return ((XssHttpServletRequestWrapperFilter) req).getOrgRequest();
}
return req;
}
}
至此,XSS攻擊問題得到解決。