|
Size: 12726
Comment:
|
← Revision 3 as of 2009-09-20 23:20:15 ⇥
Size: 12726
Comment: converted to 1.6 markup
|
| No differences found! | |
How to protect server side generated values against client side changes using a HMAC
(an attempt to fix https://issues.apache.org/jira/browse/TAPESTRY-2482)
Tapestry 5 stores some server side generated state at the client side. Example of this is t:formdata and t:state:client. Client side state however can be changed by the client (which is pretty obvious ;). Some, including me, see this as a possible security vulnerability. Even though sensitive data (which should not accesible to the user) should never be stored client side, allowing changes to the client side state can still be problematic. One way to prevent client side changes is the addition of a secure checksum that is derived from the client side data. If the data is changed the checksum no longer matches and the server can detect that the state has been changed. The checksum need to be generated in such a way that the client is unable to generate a checksum him/her self. The standard choice for such a checksum is the "keyed-Hash Message Authentication Code" (HMAC) (See: http://en.wikipedia.org/wiki/HMAC).
The following part will briefly explain how you can add an automatic checksum to sensitive elements.
Application State Object (ASO)
The HMAC requires a secret for the calculation of the checksum. In principle you can use one key for all clients (client is defined as having a session). But, in order to make a 'reply attack' less likely (will come to that later) I have chosen to give each use his/her own secret key. It depends on you requirements whether this is acceptable (memory wise).
1 /**
2 * Application state object which will hold the randomly generated key used for the
3 * calculation of a HMAC checksum.
4 *
5 * Note: This won't protect you against a 'reply attack'.
6 *
7 * @author Martijn Brinkers
8 *
9 */
10 public class HMAC
11 {
12 private final String algorithm;
13 private final SecretKey key;
14
15 public HMAC(@Inject @Value("${hmac.algorithm}") String algorithm)
16 throws NoSuchAlgorithmException
17 {
18 Check.notNull(algorithm, "algorithm");
19
20 this.algorithm = algorithm;
21
22 KeyGenerator kg = KeyGenerator.getInstance(algorithm);
23
24 key = kg.generateKey();
25 }
26
27 public SecretKey getKey() {
28 return key;
29 }
30
31 public String calculateHMAC(String input)
32 throws NoSuchAlgorithmException, InvalidKeyException
33 {
34 Mac mac = Mac.getInstance(algorithm);
35
36 mac.init(key);
37
38 byte[] hmac = mac.doFinal(MiscStringUtils.getAsciiBytes(input));
39
40 return toMaxRadix(hmac);
41 }
42
43 public static String toMaxRadix(byte bytes[])
44 {
45 BigInteger bigInt = new BigInteger(1, bytes);
46 return bigInt.toString(36);
47 }
48 }
Adding the HMAC hidden field and checking the HMAC
The HMAC need to be calculated after the page has been rendered. The calculated HMAC will need to be added as a hidden field to the page. Adding the hidden field requires a MarkupRendererFilter and checking the HMAC requires a ComponentEventRequestFilter. The following interface and implementation shows you how HMAC is 'injected' and how the HMAC is checked when a request comes in. The HMAC!FilterImpl requires an array of Strings with the element names that need to be protected with a HMAC. When an element that requires protection is found in the generated page the HMAC of the elements value is calculated and added as a hidden field. When the request comes in, the MHAC of the value is again calculated and compared to a list of HMACs from the request.
1 /**
2 * Filter that calculates the HMAC of a elements value and add the HMAC to the generated page
3 * as a hidden element. When a page is activated (for example by a post) the HMAC from the request
4 * is checked against a (newly) calculated HMAC. If the HMACs differ it means that the value
5 * has been changed and the user is redirected to another page that to report this.
6 *
7 * @author Martijn Brinkers
8 *
9 */
10 public class HMACFilterImpl implements HMACFilter
11 {
12 private final static Logger logger = LoggerFactory.getLogger(HMACFilterImpl.class);
13
14 /*
15 * The set of all element names that need to be protected by a checksum
16 */
17 private Set<String> protectedElements = new HashSet<String>();
18
19 public static String HMAC_PARAMETER = "hmac-checksum";
20
21 private final ApplicationStateManager asm;
22 private final Request request;
23 private final Response response;
24 private final LinkFactory linkFactory;
25 private final RequestPageCache requestPageCache;
26
27 /*
28 * The page to redirect to when secureID is incorrect
29 */
30 private final String redirectTo;
31
32 /*
33 * Exception thrown when HMAC is incorrect
34 */
35 private static class IncorrectHMACException extends Exception
36 {
37 private static final long serialVersionUID = -8133828090623176301L;
38
39 public IncorrectHMACException(String message) {
40 super(message);
41 }
42 }
43
44 public HMACFilterImpl(ApplicationStateManager asm, Request request, Response response,
45 LinkFactory linkFactory, RequestPageCache requestPageCache, String redirectTo,
46 String... protectedElements)
47 {
48 Check.notNull(asm, "asm");
49 Check.notNull(request, "request");
50 Check.notNull(response, "response");
51 Check.notNull(linkFactory, "linkFactory");
52 Check.notNull(requestPageCache, "requestPageCache");
53 Check.notNull(redirectTo, "redirectTo");
54
55 this.asm = asm;
56 this.request = request;
57 this.response = response;
58 this.linkFactory = linkFactory;
59 this.requestPageCache = requestPageCache;
60 this.redirectTo = redirectTo;
61
62 for (String protectedElement : protectedElements)
63 {
64 if (protectedElement == null) {
65 continue;
66 }
67
68 protectedElement = protectedElement.trim().toLowerCase();
69
70 this.protectedElements.add(protectedElement);
71 }
72 }
73
74 public void renderMarkup(MarkupWriter writer, MarkupRenderer renderer)
75 {
76 renderer.renderMarkup(writer);
77
78 Document document = writer.getDocument();
79
80 if (document != null)
81 {
82 Element root = document.getRootElement();
83
84 if (root != null)
85 {
86 LinkedList<Element> queue = new LinkedList<Element>();
87
88 queue.add(root);
89
90 while (!queue.isEmpty())
91 {
92 Element element = queue.removeFirst();
93
94 if (element == null) {
95 continue;
96 }
97
98 String elementName = element.getAttribute("name");
99
100 if (elementName != null) {
101 elementName = elementName.trim().toLowerCase();
102 }
103
104 if (protectedElements.contains(elementName))
105 {
106 /*
107 * It's a protected item so we should calculate the HMAC of the value
108 */
109 String value = element.getAttribute("value");
110
111 String hmac;
112
113 try {
114 hmac = calculateHMAC(value);
115 }
116 catch (InvalidKeyException e) {
117 throw new MimesecureRuntimeException(e);
118 }
119 catch (NoSuchAlgorithmException e) {
120 throw new MimesecureRuntimeException(e);
121 }
122
123 /*
124 * Add the HMAC checksum as a hidden element
125 */
126 element.element("input",
127 "type", "hidden",
128 "name", HMAC_PARAMETER,
129 "value", hmac);
130 }
131
132 for (Node n : element.getChildren())
133 {
134 Element child = null;
135
136 if (n instanceof Element) {
137 child = (Element) n;
138 }
139
140 if (child != null) queue.addLast(child);
141 }
142 }
143 }
144 }
145 }
146
147 public void handle(ComponentEventRequestParameters parameters,
148 ComponentEventRequestHandler handler)
149 throws IOException
150 {
151 Page page = requestPageCache.get(parameters.getActivePageName());
152
153 try {
154 if (isHMACProtected(page))
155 {
156 /*
157 * We will build a set of all the HMACS we can find in the request
158 */
159 String[] hmacParameters = request.getParameters(HMAC_PARAMETER);
160
161 Set<String> hmacs = new HashSet<String>();
162
163 if (hmacParameters != null)
164 {
165 for (String hmac : hmacParameters) {
166 hmacs.add(hmac);
167 }
168 }
169
170 for (String protectedElement : protectedElements)
171 {
172 /*
173 * There can be more than one protected value per element
174 */
175 String[] protectedValues = request.getParameters(protectedElement);
176
177 if (protectedValues != null)
178 {
179 for (String protectedValue : protectedValues)
180 {
181 String hmac = calculateHMAC(protectedValue);
182
183 if (!hmacs.contains(hmac)) {
184 throw new IncorrectHMACException("The hmac " + hmac + " is incorrect");
185 }
186 }
187 }
188 }
189 }
190
191 /*
192 * Not protected or checksum is correct so continue
193 */
194 handler.handle(parameters);
195 }
196 catch(IncorrectHMACException e)
197 {
198 logger.warn(e.getMessage());
199
200 Link link = linkFactory.createPageLink(redirectTo, false);
201
202 response.sendRedirect(link);
203 }
204 catch (InvalidKeyException e) {
205 throw new IOException(e);
206 }
207 catch (NoSuchAlgorithmException e) {
208 throw new IOException(e);
209 }
210 }
211
212 private String calculateHMAC(String value)
213 throws InvalidKeyException, NoSuchAlgorithmException
214 {
215 if (value == null) {
216 value = "";
217 }
218
219 return asm.get(HMAC.class).calculateHMAC(value);
220 }
221
222 private boolean isHMACProtected(Page page)
223 {
224 /*
225 * For now all actions are protected. We can always create a special Annotation when
226 * we need to specify which pages/actions are protected.
227 */
228 return true;
229 }
230 }
Application Module
The following items need to be added to your application module (or to a new module)
1 public void contributeComponentEventRequestHandler(OrderedConfiguration<ComponentEventRequestFilter> configuration,
2 HMACFilter hMACFilter)
3 {
4 configuration.add("HMACFilter", hMACFilter, "after:*");
5 }
6
7 public static HMACFilter buildHMACFilter(ApplicationStateManager asm,
8 Request request, Response response, LinkFactory linkFactory,
9 RequestPageCache requestPageCache, @Inject @Value("${hmac.redirectTo}") String redirectTo)
10 {
11 String[] protectedElements = {Form.FORM_DATA, "t:state:client"};
12
13 HMACFilter filter = new HMACFilterImpl(asm, request, response, linkFactory, requestPageCache,
14 redirectTo, protectedElements);
15
16 return filter;
17 }
18
19 public void contributeMarkupRenderer(OrderedConfiguration<MarkupRendererFilter> configuration,
20 HMACFilter hMACFilter)
21 {
22 configuration.add("HMACFilter", hMACFilter, "after:UploadException", "before:Ajax");
23 }
24
25 public static void contributeFactoryDefaults(
26 MappedConfiguration<String, String> configuration)
27 {
28 configuration.add("hmac.algorithm", "HmacSHA1");
29 configuration.add("hmac.redirectTo", "accessdenied");
30 }
The default algorithm for the HMAC is HmacSHA1. The hmac.redirectTo parameter sets the page to which is redirected when the HMACs do not match.
Replay Attack
The HMAC checksum protects you against changes done by the client but not against a replay attack (see http://en.wikipedia.org/wiki/Replay_attack) because the checksum calculation is kind of static. If you want to prevent replay-attacks you can for example add a counter that is increased for each request to the HMAC calculation process. The example above uses a random key stored in the user session. After the user session has ended all of the HMACs previously generated become invalid. A reply attack is therefore only possible within one session (and thus won't protect you against the current user)
Martijn Brinkers (m.brinkers@pobox.com)