001    package hirondelle.web4j.security;
002    
003    import javax.servlet.Filter;
004    import javax.servlet.FilterChain;
005    import javax.servlet.FilterConfig;
006    import javax.servlet.ServletException;
007    import javax.servlet.ServletRequest;
008    import javax.servlet.ServletResponse;
009    import javax.servlet.http.HttpServletRequest;
010    import javax.servlet.http.HttpSession;
011    import java.security.MessageDigest;
012    import java.security.NoSuchAlgorithmException;
013    import java.util.Random;
014    import java.io.IOException;
015    import java.util.logging.Logger;
016    
017    import hirondelle.web4j.database.DAOException;
018    import hirondelle.web4j.model.Id;
019    import hirondelle.web4j.util.Util;
020    import hirondelle.web4j.database.SqlId;
021    
022    /**
023     Protect your application from a 
024     <a href='http://en.wikipedia.org/wiki/Cross-site_request_forgery'>Cross Site Request Forgery</a> (CSRF).
025    
026     <P>Please see the package overview for important information regarding CSRF attacks, and security in general.
027     
028     <P>This filter maintains various items needed to protect against CSRF attacks. It acts both as a 
029     pre-processor and as a post-processor. The behavior of this class is controlled by detecting two important events: 
030     <ul>
031      <li>the creation of new sessions (which does <i>not</i> necessarily imply a successful user login has also occured)
032      <li>a successful user login (which <i>does</i> imply a session has also been created)
033     </ul> 
034    
035     <h4>Pre-processing</h4>
036     When <i>a new session</i> is detected (but not necessarily a user login), then this class will do the following :
037     <ul>
038     <li>calculate a random form-source id, and place it in session scope, under the key {@link #FORM_SOURCE_ID_KEY}. 
039     This value is difficult to guess.
040     <li>wrap the response in a custom wrapper, to implement the post-processing performed by this filter (see below)
041     </ul>
042     
043     In addition, if <i>a new user login</i> is detected, then this class will do the following :
044     <ul>
045     <li>if there is any 'old' form-source id, place it in session scope as well, under the 
046     key {@link #PREVIOUS_FORM_SOURCE_ID_KEY}. The 'old' form-source id is simply the form-source id 
047     used in the <em>immediately preceding session for the same user</em>.
048     <li>place in session scope an object which will store the form-source id when the session expires or is invalidated, under 
049     the key {@link #FORM_SOURCE_DAO_KEY}.
050     </ul> 
051     
052     <P>The above behavior of this class upon user login requires interaction with your database. 
053     It's configured in <tt>web.xml</tt> using two items : 
054     <tt>FormSourceIdRead</tt> and <tt>FormSourceIdWrite</tt>. These two items are 
055     {@link hirondelle.web4j.database.SqlId} references. 
056     They tell this class which SQL statements to use when reading and writing form-source ids 
057     to the database. As usual, these {@link SqlId} items must be declared somewhere in your 
058     application as <tt>public static final</tt> fields, and the corresponding SQL statements 
059     must appear somewhere in an <tt>.sql</tt> file.
060     
061     <P>(Please see these items in the example application for an illustration : <tt>web.xml</tt>, 
062     <tt>UserDAO</tt>, and <tt>csrf.sql</tt>.) 
063     
064     <h4>Post-processing</h4>
065     If a session is present, then this class will use a custom response wrapper to alter the response:
066     <ul>
067     <li>if the response has <tt>content-type</tt> of <tt>text/html</tt> (or <tt>null</tt>), then scan 
068     the response for all {@code <FORM>} tags with <tt>method='POST'</tt>. 
069     <li>for each such {@code <FORM>} tag, add a hidden parameter in the following style :
070    <PRE>&lt;input type='hidden' name='web4j_key_for_form_source_id' value='151jdk65654dasdf545sadf6a5s4f'&gt;</PRE>
071    </ul>
072     
073     The name of the hidden parameter is taken from {@link #FORM_SOURCE_ID_KEY}, 
074     and the <tt>value</tt> of that hidden parameter is the random token created during the pre-processing stage.
075    
076    <h4>ApplicationFirewall</h4>
077    This class cooperates closely with {@link hirondelle.web4j.security.ApplicationFirewallImpl}. It is the 
078    firewall which performs the actual test to make sure the POSTed form came from your web app. 
079    
080     <h4>Warning Regarding Error Pages</h4>
081     This Filter uses a wrapper for the response. When a Filter wraps the response, the error page 
082     customization defined by <tt>web.xml</tt> will likely not function. 
083     (This may be a defect of the Servlet API itself - see section 9.9.3.) That is, when an error occurs when using this 
084     Filter, the generic error pages defined by the container may be served, instead of the custom 
085     error pages you have configured in <tt>web.xml</tt>.
086    */
087    public class CsrfFilter implements Filter {
088    
089      /** 
090       <em>Key</em> for item stored in session scope, and also <em>name</em> of hidden 
091       request parameter added to POSTed forms.
092       
093       <P>Value - {@value}.
094       <P>The <em>value</em> of this item is generated randomly for each new user login, and contains a 
095       simple token that is hard to guess. Each POSTed form will be required by {@link ApplicationFirewallImpl} 
096       to include a hidden parameter of this <em>name</em>, and the <em>value</em> of such hidden parameters 
097       are matched to the corresponding item stored in session scope under the same key. These checks verify that  
098       POSTed forms have come from a trusted source.
099      */
100      public static final String FORM_SOURCE_ID_KEY = "web4j_key_for_form_source_id";
101    
102      /** 
103       Key for item stored in session scope.
104       
105       <P>Value - {@value}.
106       <P>The value of this item is retrieved from the database for each new user login, and 
107       represents the form-source id for the user's <em>immediately preceding</em> session. 
108       When a match of form-source id against {@link #FORM_SOURCE_ID_KEY} fails, then a second 
109       match is attempted against this item.
110       
111       <P>Please see the package description for an explanation of why this is necessary.
112      */
113      public static final String PREVIOUS_FORM_SOURCE_ID_KEY = "web4j_key_for_previous_form_source_id";
114      
115      /**
116       Key for item stored in session scope.
117         
118       <P>Value - {@value}.
119       <P>This item points to an {@link javax.servlet.http.HttpSessionBindingListener} object placed in each new session. 
120       When the session ends, that object will be unbound from the session, and will save the user's current form-source id 
121       to the database, for future use.  
122      */
123      public static final String FORM_SOURCE_DAO_KEY = "web4j_key_for_form_source_dao";
124    
125      /** 
126       Read in filter configuration. 
127       
128       <P>Reads in {@link hirondelle.web4j.database.SqlId} references used to read and write the user's form-source id.
129       <P>See class comment and package-level description for further information.
130      */
131      public void init(FilterConfig aFilterConfig)  {
132        fLogger.config("INIT : " + this.getClass().getName() + ". Reading in SqlIds for reading and writing form-source ids.");
133        String read_sql = aFilterConfig.getInitParameter("FormSourceIdRead");
134        String write_sql = aFilterConfig.getInitParameter("FormSourceIdWrite");
135        checkValidSqlId(read_sql);
136        checkValidSqlId(write_sql);
137        CsrfDAO.init(read_sql, write_sql);
138      }
139      
140      /** This implementation does nothing.  */
141      public void destroy() {
142        fLogger.config("DESTROY : " + this.getClass().getName());
143      }
144      
145      /**
146       Protect against CSRF attacks.
147      
148       <P>See class comment and package-level description for further information.
149      */
150      public void doFilter(ServletRequest aRequest, ServletResponse aResponse, FilterChain aChain) throws IOException, ServletException {
151        fLogger.fine("START CSRF Filter.");
152        addItemsForNewSessions((HttpServletRequest) aRequest);
153        CsrfResponseWrapper wrapper = new CsrfResponseWrapper(aResponse, aRequest);
154        aChain.doFilter(aRequest, wrapper); //AppFirewall and BadReq
155        if( wrapper.hasError() ) {
156          fLogger.fine("Error detected. Error code : " + wrapper.getErrorCode());
157          fLogger.fine("Warning: this Filter uses a wrapped response. Error code mapping configured in web.xml usually doesn't work with wrapped responses.");
158          //fLogger.fine("Is committed : " + wrapper.isCommitted()); //YES - committed when the controller calls sendError().
159          //Unfortunately, when using *any* response wrapper, the normal error-page mechanism is not 
160          //activated by the container - you see the server's generic error page.
161        }
162        else {
163          fLogger.finer("No error detected by CsrfFilter.");
164        }
165        fLogger.fine("END CSRF Filter.");
166      }
167      
168      /**
169       Add a CSRF token to an existing session <i>that has no user login</i>.
170       
171       <P><i>This method is called only when a session created by an Action, instead of the usual login mechanism.</i>
172       See {@link hirondelle.web4j.action.ActionImpl#createSessionAndCsrfToken()} for important information.  
173      */
174      public void addCsrfToken(HttpServletRequest aRequest) throws ServletException {
175        addItemsForNewSessions(aRequest);
176      }
177      
178      // PRIVATE //
179      
180      //WARNING : Filters always need to be thread-safe !!
181      
182      private static final Logger fLogger = Util.getLogger(CsrfFilter.class);
183      private static final boolean DO_NOT_CREATE = false;
184      
185      private static void checkValidSqlId(String aSqlId){
186        if ( ! Util.textHasContent(aSqlId) ) {
187          String message = "SqlId required as Filter init-param, but has no content: " + Util.quote(aSqlId); 
188          fLogger.severe(message);
189        }
190      }
191      
192      private void addItemsForNewSessions(HttpServletRequest aRequest) throws ServletException {
193        HttpSession session = aRequest.getSession(DO_NOT_CREATE);
194        if ( sessionExists(session) ){
195          if ( hasNoFormSourceIdInSession(session) ){
196            Id currentFormSourceId = calcFormSourceId();
197            addFormSourceIdToSession(session, currentFormSourceId);
198            if( userHasLoggedIn(aRequest) ){
199              CsrfDAO formSourceDAO = new CsrfDAO(aRequest.getUserPrincipal().getName(), currentFormSourceId);
200              addPreviousFormSourceIdToSession(session, formSourceDAO);          
201              addFormSourceDAOToSession(session, formSourceDAO);
202            }
203          }
204        }
205      }
206      
207      private boolean sessionExists(HttpSession aSession){
208        return aSession != null;
209      }
210      
211      private boolean hasNoFormSourceIdInSession(HttpSession aSession){
212        return aSession.getAttribute(FORM_SOURCE_ID_KEY) == null;
213      }
214    
215      private boolean userHasLoggedIn(HttpServletRequest aRequest){
216        return aRequest.getUserPrincipal() != null;
217      }
218      
219      private void addFormSourceIdToSession(HttpSession aSession, Id aCurrentFormSourceId) {
220        fLogger.fine("Adding new form-source id to user's session.");
221        aSession.setAttribute(FORM_SOURCE_ID_KEY, aCurrentFormSourceId);
222      }
223      
224      private Id calcFormSourceId(){
225        String token = getHashFor( getRandomNumber().toString() );
226        return new Id(token);    
227      }
228      
229      private void addPreviousFormSourceIdToSession(HttpSession aSession, CsrfDAO aDAO) throws ServletException {
230        fLogger.fine("Adding previous form-source id to session.");
231        try {
232          Id previousFormSourceId = aDAO.fetchPreviousFormSourceId();
233          if( previousFormSourceId == null ) {
234            fLogger.fine("No previous form-source id found.");
235          }
236          else {
237            fLogger.fine("Adding previous form-source id to session.");
238            aSession.setAttribute(PREVIOUS_FORM_SOURCE_ID_KEY, previousFormSourceId);
239          }
240        }
241        catch (DAOException ex){
242          throw new ServletException("Cannot fetch previous form-source id from database.", ex);
243        }
244      }
245      
246      private void  addFormSourceDAOToSession(HttpSession aSession, CsrfDAO aDAO) {
247        fLogger.fine("Adding CsrfDAO object to session.");
248        aSession.setAttribute(FORM_SOURCE_DAO_KEY, aDAO);
249      }
250      
251      private synchronized Long getRandomNumber() {
252        Random random = new Random();
253        return random.nextLong();
254      }
255      
256      private String getHashFor(String aText) {
257        String result = null;
258        try {
259          MessageDigest sha = MessageDigest.getInstance("SHA-1");
260          byte[] hashOne = sha.digest(aText.getBytes());
261          result = hexEncode(hashOne);
262        }
263        catch (NoSuchAlgorithmException ex){
264          String message = "MessageDigest cannot find SHA-1 algorithm."; 
265          fLogger.severe(message);
266          throw new RuntimeException(message);
267        }
268        return result;
269      }
270      
271      /**
272       The byte[] returned by MessageDigest does not have a nice
273       textual representation, so some form of encoding is usually performed.
274      
275       This implementation follows the example of David Flanagan's book
276       "Java In A Nutshell", and converts a byte array into a String
277       of hex characters.
278      */
279      static private String hexEncode( byte[] aInput){
280        StringBuilder result = new StringBuilder();
281        char[] digits = {'0', '1', '2', '3', '4','5','6','7','8','9','a','b','c','d','e','f'};
282        for (int idx = 0; idx < aInput.length; ++idx) {
283          byte b = aInput[idx];
284          result.append( digits[ (b&0xf0) >> 4 ] );
285          result.append( digits[ b&0x0f] );
286        }
287        return result.toString();
288      }
289    }