Differences between revisions 2 and 3
Revision 2 as of 2008-11-07 09:45:12
Size: 12726
Comment:
Revision 3 as of 2009-09-20 23:20:15
Size: 12726
Editor: localhost
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 public interface HMACFilter extends ComponentEventRequestFilter, MarkupRendererFilter {
   2         /*
   3          * For now we do not need any extra methods.
   4          */
   5 }

   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)

Tapestry5PreventClientSideChanges (last edited 2009-09-20 23:20:15 by localhost)