永遠不要相信來自客戶端的資料,例如,基於安全考量,輸入中若有 HTML 標籤應該過濾掉,或者將 <
、>
角括號置換為 HTML 替代字元 <
與 >
。
你可以使用過濾器的方式,將使用者請求參數中的角括號字元進行替換。但問題在於,雖然你可以使用 HttpServletRequest
的 getParameter()
取得請求參數值,但就是沒有一個像 setParameter()
的方法,可以讓你將處理過後的請求參數重新設定給 HttpServletRequest
。
你也許會想要親自實作 HttpServletRequest
介面,讓 getParameter()
傳回過濾後的請求參數值,但這麼作的話,HttpServletRequest
介面上所有定義的方法都要實作,實作所有的方法是件很麻煩的事。所幸,有個 HttpServletRequestWrapper
幫你實作了 HttpServletRequest
介面,你只要繼承 HttpServletRequestWrapper
類別,並撰寫你想要重新定義的方法即可。相對應於 ServletRequest
介面,也有個 ServletRequestWrapper
類別可以使用。
以下的範例透過繼承 HttpServletRequestWrapper
實作了一個請求包裹器,可以將請求參數中的角括號替換為替代字元。
package cc.openhome;
import java.util.*;
import javax.servlet.http.*;
public class CharacterRequestWrapper extends HttpServletRequestWrapper {
private Map<String, String> escapes;
public CharacterRequestWrapper(HttpServletRequest request, Map<String, String> escapes) {
super(request);
this.escapes = escapes;
}
@Override
public String getParameter(String name) {
return Optional.ofNullable(getRequest().getParameter(name))
.map(this::escape)
.orElse(null);
}
private String escape(String value) {
String result = value;
for(String origin : escapes.keySet()) {
result = result.replaceAll(origin, escapes.get(origin));
}
return result;
}
}
在繼承 HttpServletRequestWrapper
之後,必須定義建構式,透過 super()
來呼叫父類別建構式,並傳入想要包裹的原請求物件,之後若想取得被包裹的原請求物件,則可以透過 getRequest()
方法來取得。建構式中也傳入了一個 Map
物件,這個物件中「鍵」的部份為想要替換的原字元,「值」的部份則是對應的替代字元。
如果程式中想要從包裹器請求物件的 getParameter()
取得請求參數,則先從原請求物件的 getParameter()
取得值,然後進行字元替換。字元替換的方法撰寫在 escape()
方法之中。
可以使用這個請求包裹器類別搭配過濾器,以進行字元過濾的服務。例如:
package cc.openhome;
import java.io.*;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.*;
@WebFilter(
urlPatterns={"/guestbook"},
initParams={
@WebInitParam(
name="ESCAPE_LIST",
value="/WEB-INF/escapes.txt"
)
}
)
public class CharacterFilter extends HttpFilter {
private Map<String, String> escapes = new HashMap<>();
@Override
public void init() throws ServletException {
try(BufferedReader reader = new BufferedReader(
new InputStreamReader(
getServletContext()
.getResourceAsStream(getInitParameter("ESCAPE_LIST"))))) {
String input = null;
while ((input = reader.readLine()) != null) {
String[] tokens = input.split("\t");
escapes.put(tokens[0], tokens[1]);
}
} catch (IOException ex) {
Logger.getLogger(CharacterFilter.class.getName())
.log(Level.SEVERE, null, ex);
}
}
@Override
protected void doFilter(HttpServletRequest request, HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
chain.doFilter(new CharacterRequestWrapper(request, escapes), response);
}
}
這個過濾器過濾字元的依據是個字元表檔案,檔案名稱可以透過過濾器初始參數來設定,並透過 getInitParameter()
來取得檔案名稱。讀入的字元對應將分別填入 Map
物件的「鍵」與「值」。
接下來在 doFilter()
之中,建立 CharacterRequestWrapper
實例,並將原請求物件傳入建構式進行包裹。然後將 CharacterRequestWrapper
實例傳入 FilterChain
的 doFilter()
中作為請求物件。
之後的 Filter 或 Servlet 實例,不需要也不會知道請求物件已經被包裹,在必須取得請求參數時,一樣呼叫 getParameter()
即可。
字元過濾表檔案必須放在 WEB-INF 之中,檔名為 escapes.txt,你可以自行新增要過濾的字元到檔案中,字元之間以跳位字元(\t
)分隔。例如檔案內容如下:
< <
> >
當你將這個過濾器掛上去之後,如果有使用者試圖輸入 HTML 標籤,由於角括號都被替換為替代字元。