OSDN Git Service

CSRFインターセプターをマシに。
[rabbit-bts/RabbitBTS.git] / src / jp / sourceforge / rabbitBTS / interceptors / CSRFInterceptor.java
1 /* vim:set ts=4 sts=4 sw=4 noet fenc=utf-8:
2
3    Copyright 2009 senju@users.sourceforge.jp
4
5    Licensed under the Apache License, Version 2.0 (the "License");
6    you may not use this file except in compliance with the License.
7    You may obtain a copy of the License at
8
9        http://www.apache.org/licenses/LICENSE-2.0
10
11    Unless required by applicable law or agreed to in writing, software
12    distributed under the License is distributed on an "AS IS" BASIS,
13    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14    See the License for the specific language governing permissions and
15    limitations under the License.
16  */
17
18 package jp.sourceforge.rabbitBTS.interceptors;
19
20 import java.util.ArrayList;
21 import java.util.Date;
22 import java.util.HashMap;
23 import java.util.List;
24 import java.util.Map;
25
26 import javax.servlet.http.HttpServletRequest;
27 import javax.servlet.http.HttpServletResponse;
28
29 import jp.sourceforge.rabbitBTS.Sht;
30 import jp.sourceforge.rabbitBTS.controllers.IController;
31
32 import org.apache.commons.lang.RandomStringUtils;
33 import org.apache.commons.lang.StringUtils;
34 import org.springframework.web.servlet.ModelAndView;
35 import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
36
37 /**
38  * CSRF対策用インターセプター
39  */
40 public class CSRFInterceptor extends HandlerInterceptorAdapter {
41         private int expireInSecond;
42
43         /**
44          * POSTの場合チェック処理
45          */
46         @Override
47         public boolean preHandle(HttpServletRequest request,
48                         HttpServletResponse response, Object handler) throws Exception {
49                 if (request.getMethod().equals("POST")
50                                 && handler instanceof IController) {
51                         final IController c = (IController) handler;
52                         final CsrfChecker checker = new CsrfChecker(request);
53                         if (checker.checkTokenValid()) {
54                                 c.setCsrfSafe(true);
55                         } else {
56                                 c.setCsrfSafe(false);
57                                 Sht.log(this).warning("CSRF detected.");
58                         }
59                 }
60                 return true;
61         }
62
63         /**
64          * セッションとMAVにトークンを格納しておく
65          */
66         @Override
67         public void postHandle(HttpServletRequest request,
68                         HttpServletResponse response, Object handler, ModelAndView mav)
69                         throws Exception {
70                 // リダイレクトする場合は不要
71                 if (mav != null) {
72                         if (!StringUtils.startsWith(mav.getViewName(), "redirect:")) {
73                                 final CsrfChecker checker = new CsrfChecker(request);
74                                 final String token = checker.saveNewToken();
75                                 mav.addObject("secureToken", token);
76                         }
77                 }
78
79                 if (request.getMethod().equals("POST")
80                                 && handler instanceof IController) {
81                         // きちんとCSRFチェックが行われているかチェックする
82                         final IController c = (IController) handler;
83                         if (!c.isCsrfChecked()) {
84                                 Sht.log(this).severe("CSRFチェックを行っていないPOST");
85                         }
86                         assert c.isCsrfChecked() : "CSRFチェックを行っていないPOST";
87                 }
88         }
89
90         /**
91          * チェック用クラス
92          */
93         class CsrfChecker {
94                 private final HttpServletRequest req;
95                 private Map<String, Date> tokens;
96
97                 /**
98                  * コンストラクタ
99                  * 
100                  * <p>
101                  * セッションにトークンのリストが存在しない場合、新規に作成しセッションに保存する。
102                  * 
103                  * @param request
104                  */
105                 @SuppressWarnings("unchecked")
106                 public CsrfChecker(HttpServletRequest request) {
107                         this.req = request;
108                         this.tokens = (Map<String, Date>) request.getSession()
109                                         .getAttribute("tokens");
110                         if (this.tokens == null) {
111                                 this.tokens = new HashMap<String, Date>();
112                                 request.getSession().setAttribute("tokens", this.tokens);
113                         }
114                 }
115
116                 /**
117                  * トークンをセッションに保存する。
118                  * 
119                  * @return 保存された新規トークン
120                  */
121                 public String saveNewToken() {
122                         final String token = RandomStringUtils.randomAlphanumeric(128);
123                         this.tokens.put(token, new Date());
124                         return token;
125                 }
126
127                 /**
128                  * チェックを行う
129                  * 
130                  * @return 正しいパラメータが送信された場合true
131                  */
132                 public boolean checkTokenValid() {
133                         final String reqToken = this.req.getParameter("secureToken");
134                         final Date datenow = new Date();
135                         // トークンチェック
136                         boolean found = false;
137                         if (this.tokens.containsKey(reqToken)) {
138                                 found = checkExpire(datenow, this.tokens.get(reqToken));
139                         }
140
141                         // 削除チェック
142                         if (this.tokens.size() > 30) {
143                                 final List<String> removeList = new ArrayList<String>();
144                                 for (final String token : this.tokens.keySet()) {
145                                         final Date created = this.tokens.get(token);
146                                         if (!this.checkExpire(datenow, created)) {
147                                                 // 期限切れの場合
148                                                 removeList.add(token);
149                                                 Sht.log(this).finer("delete token: " + token);
150                                         }
151                                 }
152                                 for (final String token : removeList) {
153                                         this.tokens.remove(token);
154                                 }
155                         }
156
157                         return found;
158                 }
159
160                 /**
161                  * 期限切れかチェックする。
162                  * 
163                  * @param datenow
164                  * @param created
165                  * @return 期限以内の場合true
166                  */
167                 private boolean checkExpire(Date datenow, Date created) {
168                         final long ageInMils = datenow.getTime() - created.getTime();
169                         return ageInMils / 1000 < CSRFInterceptor.this.expireInSecond;
170                 }
171         }
172
173         /**
174          * @param expireInSecond
175          *            the expireInSecond to set
176          */
177         public void setExpireInSecond(int expireInMinute) {
178                 this.expireInSecond = expireInMinute;
179         }
180
181 }