1   package com.atlassian.core.filters;
2   
3   import java.io.IOException;
4   import java.util.Arrays;
5   import javax.servlet.http.Cookie;
6   import javax.servlet.http.HttpServletResponse;
7   import javax.servlet.http.HttpServletResponseWrapper;
8   
9   /**
10   * A wrapper for a {@link javax.servlet.http.HttpServletResponse} that sanitises all mutations to the header of the
11   * response to ensure that no suspect values are being written.
12   *
13   * The protocol for sanitising header values is essentially to replace any encountered carriage return or new line
14   * characters with a single space.
15   *
16   * @since v4.2
17   */
18  public class HeaderSanitisingResponseWrapper extends HttpServletResponseWrapper
19  {
20      private static final char[] DISALLOWED_CHARS = new char[] {'\r', '\n'};
21      private static final char REPLACEMENT_CHAR = ' ';
22  
23      private final char[] disallowedChars;
24      private final char replacementChar;
25  
26      public HeaderSanitisingResponseWrapper(HttpServletResponse httpServletResponse)
27      {
28          this(httpServletResponse, DISALLOWED_CHARS, REPLACEMENT_CHAR);
29      }
30  
31      /**
32       * For testing purposes.
33       *
34       * @param httpServletResponse the response we are wrapping
35       * @param disallowedChars the characters that will be replaced
36       * @param replacementChar the replacement char
37       */
38      HeaderSanitisingResponseWrapper(final HttpServletResponse httpServletResponse, final char[] disallowedChars, final char replacementChar)
39      {
40          super(httpServletResponse);
41          Arrays.sort(disallowedChars);
42          this.disallowedChars = disallowedChars;
43          this.replacementChar = replacementChar;
44      }
45  
46      /**
47       * Sanitises cookie value before adding it to the response. Note that cookie names are immutable and so cannot be
48       * sanitised here.
49       *
50       * @param cookie the cookie to add to the header.
51       */
52      public void addCookie(Cookie cookie)
53      {
54          if (cookie != null)
55          {
56              cookie.setValue(cleanString(cookie.getValue()));
57          }
58          super.addCookie(cookie);
59      }
60  
61      public void setContentType(final String contentType)
62      {
63          super.setContentType(cleanString(contentType));
64      }
65  
66      public void setDateHeader(final String name, final long value)
67      {
68          super.setDateHeader(cleanString(name), value);
69      }
70  
71      public void addDateHeader(final String name, final long value)
72      {
73          super.addDateHeader(cleanString(name), value);
74      }
75  
76      public void setHeader(final String name, final String value)
77      {
78          super.setHeader(cleanString(name), cleanString(value));
79      }
80  
81      public void addHeader(final String name, final String value)
82      {
83          super.addHeader(cleanString(name), cleanString(value));
84      }
85  
86      public void setIntHeader(final String name, final int value)
87      {
88          super.setIntHeader(cleanString(name), value);
89      }
90  
91      public void addIntHeader(final String name, final int value)
92      {
93          super.addIntHeader(cleanString(name), value);
94      }
95  
96      public void sendRedirect(final String location) throws IOException
97      {
98          super.sendRedirect(cleanString(location));
99      }
100 
101     public void sendError(final int code, final String message) throws IOException
102     {
103         super.sendError(code, cleanString(message));
104     }
105 
106     public void setStatus(final int code, final String status)
107     {
108         super.setStatus(code, cleanString(status));
109     }
110 
111     /**
112      * Replaces all the chars in the input string lower than {@link #disallowedChars} with {@link #replacementChar}.
113      *
114      * @param value the string to clean
115      * @return the "cleaned" string
116      */
117     String cleanString(String value)
118     {
119         if (value != null && !("".equals(value)))
120         {
121             char[] chars = value.toCharArray();
122             for (int i = 0; i < chars.length; i++)
123             {
124                 if (isDisallowedChar(chars[i]))
125                 {
126                     chars[i] = replacementChar;
127                 }
128             }
129             value = new String(chars);
130         }
131         return value;
132     }
133 
134     private boolean isDisallowedChar(final char c)
135     {
136         return Arrays.binarySearch(disallowedChars, c) >= 0;
137     }
138 }