|
1 package hirondelle.web4j.security; |
|
2 |
|
3 import hirondelle.web4j.model.Id; |
|
4 import hirondelle.web4j.util.EscapeChars; |
|
5 import hirondelle.web4j.util.Regex; |
|
6 import hirondelle.web4j.util.Util; |
|
7 |
|
8 import java.util.logging.Logger; |
|
9 import java.util.regex.Matcher; |
|
10 import java.util.regex.Pattern; |
|
11 |
|
12 import javax.servlet.http.HttpServletRequest; |
|
13 import javax.servlet.http.HttpServletResponse; |
|
14 import javax.servlet.http.HttpSession; |
|
15 |
|
16 /** Add a nonce to POSTed forms in any 'text/html' response. */ |
|
17 final class CsrfModifiedResponse { |
|
18 |
|
19 CsrfModifiedResponse(HttpServletRequest aRequest, HttpServletResponse aResponse){ |
|
20 fResponse = aResponse; |
|
21 fRequest = aRequest; |
|
22 } |
|
23 |
|
24 String addNonceTo(String aUnmodifiedResponse){ |
|
25 String result = aUnmodifiedResponse; |
|
26 if(isServingHtml() && Util.textHasContent(aUnmodifiedResponse)) { |
|
27 fLogger.fine("Adding nonce to forms having method=POST, if any."); |
|
28 result = addHiddenParamToPostedForms(aUnmodifiedResponse); |
|
29 } |
|
30 return result; |
|
31 } |
|
32 |
|
33 // PRIVATE |
|
34 |
|
35 private HttpServletRequest fRequest; |
|
36 private HttpServletResponse fResponse; |
|
37 |
|
38 /** |
|
39 Group 1 is the FORM start tag *plus the body*, and group 2 is the FORM end tag. |
|
40 Note the reluctant qualifier for group 2, to ensure multiple forms are not glommed together. |
|
41 */ |
|
42 private static final String REGEX = |
|
43 "(<form" + Regex.ALL_BUT_END_OF_TAG +"method=" + Regex.QUOTE + "POST" + Regex.QUOTE + Regex.ALL_BUT_END_OF_TAG + ">" |
|
44 + Regex.ANY_CHARS + "?)" + |
|
45 "(</form>)" |
|
46 ; |
|
47 private static final Pattern FORM_PATTERN = Pattern.compile(REGEX, Pattern.DOTALL | Pattern.CASE_INSENSITIVE); |
|
48 |
|
49 private static final String TEXT_HTML = "text/html"; |
|
50 |
|
51 /** |
|
52 Problem: this class is package private. If we use the 'regular' logger, named for this class, then the output will not |
|
53 show up. So, we use a logger attached to a closely related, public class. (Neat!) |
|
54 */ |
|
55 private static final Logger fLogger = Util.getLogger(CsrfFilter.class); |
|
56 |
|
57 /** Return true if content-type of reponse is null, or starts with 'text/html' (case-sensitive). */ |
|
58 private boolean isServingHtml(){ |
|
59 String contentType = fResponse.getContentType(); |
|
60 boolean missingContentType = ! Util.textHasContent(contentType); |
|
61 boolean startsWithHTML = Util.textHasContent(contentType) && contentType.startsWith(TEXT_HTML); |
|
62 return missingContentType || startsWithHTML; |
|
63 } |
|
64 |
|
65 private String addHiddenParamToPostedForms(String aOriginalInput) { |
|
66 StringBuffer result = new StringBuffer(); |
|
67 Matcher formMatcher = FORM_PATTERN.matcher(aOriginalInput); |
|
68 while ( formMatcher.find() ){ |
|
69 fLogger.fine("Found a POSTed form. Adding nonce."); |
|
70 formMatcher.appendReplacement(result, getReplacement(formMatcher)); |
|
71 } |
|
72 formMatcher.appendTail(result); |
|
73 return result.toString(); |
|
74 } |
|
75 |
|
76 private String getReplacement(Matcher aMatcher){ |
|
77 //escape, since '$' char may appear in input |
|
78 return EscapeChars.forReplacementString(aMatcher.group(1) + getHiddenInputTag() + aMatcher.group(2)); |
|
79 } |
|
80 |
|
81 private String getHiddenInputTag(){ |
|
82 return "<input type='hidden' name='" + getHiddenParamName() + "' value='" + getHiddenParamValue().toString() + "'>"; |
|
83 } |
|
84 |
|
85 /** |
|
86 Return the form-source id value, stored in the user's session. |
|
87 If there is no session, or if there is no form-source id in the session, throw a RuntimeException. |
|
88 */ |
|
89 private Id getHiddenParamValue(){ |
|
90 Id result = null; |
|
91 boolean DO_NOT_CREATE = false; |
|
92 HttpSession session = fRequest.getSession(DO_NOT_CREATE); |
|
93 if ( session != null ) { |
|
94 result = (Id)session.getAttribute(CsrfFilter.FORM_SOURCE_ID_KEY); |
|
95 if( result == null ){ |
|
96 String message = "Session exists, but no CSRF token value is stored in the session"; |
|
97 fLogger.severe(message); |
|
98 throw new RuntimeException(message); |
|
99 } |
|
100 } |
|
101 else { |
|
102 String message = |
|
103 "No session exists! CsrfFilter can only work when a session is present, and the user has logged in. " + |
|
104 "Ensure CsrfFilter is mapped (using url-pattern) only to URLs having mandatory login and/or a valid session." |
|
105 ; |
|
106 fLogger.severe(message); |
|
107 throw new RuntimeException(message); |
|
108 } |
|
109 return result; |
|
110 } |
|
111 |
|
112 /** Return the name of the hidden form parameter. */ |
|
113 private String getHiddenParamName(){ |
|
114 return CsrfFilter.FORM_SOURCE_ID_KEY; |
|
115 } |
|
116 } |