[seam-commits] Seam SVN: r10279 - in trunk: ui/src/main/java/org/jboss/seam/ui and 2 other directories.

seam-commits at lists.jboss.org seam-commits at lists.jboss.org
Thu Apr 2 18:52:19 EDT 2009


Author: dan.j.allen
Date: 2009-04-02 18:52:19 -0400 (Thu, 02 Apr 2009)
New Revision: 10279

Added:
   trunk/ui/src/main/java/org/jboss/seam/ui/ClientUidSelector.java
   trunk/ui/src/main/java/org/jboss/seam/ui/UnauthorizedCommandException.java
   trunk/ui/src/main/java/org/jboss/seam/ui/component/UIToken.java
   trunk/ui/src/main/java/org/jboss/seam/ui/renderkit/TokenRendererBase.java
Modified:
   trunk/doc/Seam_Reference_Guide/en-US/Controls.xml
Log:
JBSEAM-4007


Modified: trunk/doc/Seam_Reference_Guide/en-US/Controls.xml
===================================================================
--- trunk/doc/Seam_Reference_Guide/en-US/Controls.xml	2009-04-02 21:22:40 UTC (rev 10278)
+++ trunk/doc/Seam_Reference_Guide/en-US/Controls.xml	2009-04-02 22:52:19 UTC (rev 10279)
@@ -850,7 +850,48 @@
       </section> 
       
       <section>
-         <title>Dropdowns</title>
+         <title>Form support</title>
+
+         <section>
+            <title><literal>&lt;s:token&gt;</literal></title>
+
+            <para><emphasis>Description</emphasis></para>
+            <para>
+              Produces a random token that is inserted into a hidden form field
+              to help to secure JSF form posts against cross-site request
+              forgery (XSRF) attacks. Note that the browser must have cookies
+              enabled to submit forms that include this component.
+            </para>
+
+            <para><emphasis>Attributes</emphasis></para>
+            <itemizedlist>
+               <listitem>
+                  <para>
+                     <literal>requireSession</literal> &#8212; indicates
+                     whether the session id should be included in the form
+                     signature, hence binding the token to the session. This
+                     value can be set to false if the "build before restore"
+                     mode of Facelets is activated (the default in JSF 2.0).
+                     (default: false)
+                  </para>
+               </listitem>
+               <listitem>
+                  <para>
+                     <literal>enableCookieNotice</literal> &#8212; indicates
+                     that a JavaScript check should be inserted into the page
+                     to verify that cookies are enabled in the browser.  If
+                     cookies are not enabled, present a notice to the user that
+                     form posts will not work.
+                     (default: false)
+                  </para>
+               </listitem>
+            </itemizedlist>
+            <para><emphasis>Usage</emphasis></para>
+            <programlisting role="XHTML"><![CDATA[<h:form>
+   <s:token enableCookieNotice="true" requireSession="false"/>
+   ...
+</h:form>]]></programlisting>
+         </section>
          
          <section>
             <title><literal>&lt;s:enumItem&gt;</literal></title>
@@ -958,65 +999,6 @@
   <s:selectItems value="#{ages}" var="age" label="#{age}" />
 </h:selectOneMenu>]]></programlisting>
          </section>
-         
-      </section>
-      
-      <section>
-         <title>Other</title>
-         
-         <section>
-            <title><literal>&lt;s:cache&gt;</literal></title>
-
-            <para><emphasis>Description</emphasis></para>
-            <para>
-               Cache the rendered page fragment using JBoss Cache. Note that
-               <literal>&lt;s:cache&gt;</literal> actually uses the instance
-               of JBoss Cache managed by the built-in 
-               <literal>pojoCache</literal> component.
-            </para>
-            <para><emphasis>Attributes</emphasis></para>
-            <itemizedlist>
-               <listitem>
-                  <para>
-                     <literal>key</literal> &#8212; the key to cache rendered 
-                     content, often a value expression. For example, if we were 
-                     caching a page fragment that displays a document, we might 
-                     use <literal>key="Document-#{document.id}"</literal>.
-                  </para>
-               </listitem>
-               <listitem>
-                  <para>
-                     <literal>enabled</literal> &#8212; a value expression that 
-                     determines if the cache should be used.
-                  </para>
-               </listitem>
-               <listitem>
-                  <para>
-                     <literal>region</literal> &#8212; a JBoss Cache node to use
-                     (different nodes can have different expiry policies).
-                 </para>
-               </listitem>
-            </itemizedlist>
-            
-            <para><emphasis>Usage</emphasis></para>
-            <programlisting role="XHTML"><![CDATA[<s:cache key="entry-#{blogEntry.id}" region="pageFragments">
-  <div class="blogEntry">
-    <h3>#{blogEntry.title}</h3>
-    <div>
-      <s:formattedText value="#{blogEntry.body}"/>
-    </div>
-    <p>
-      [Posted on&#160;
-      <h:outputText value="#{blogEntry.date}">
-        <f:convertDateTime timezone="#{blog.timeZone}" locale="#{blog.locale}" 
-                           type="both"/>
-      </h:outputText>]
-    </p>
-  </div>
-</s:cache>]]></programlisting>
-          
-         </section>
-
 		 
          <section>
             <title><literal>&lt;s:fileUpload&gt;</literal></title>
@@ -1131,7 +1113,66 @@
               contentType="#{register.pictureContentType}" />]]></programlisting>
               
          </section>
+         
+      </section>
+      
+      <section>
+         <title>Other</title>
+         
+         <section>
+            <title><literal>&lt;s:cache&gt;</literal></title>
 
+            <para><emphasis>Description</emphasis></para>
+            <para>
+               Cache the rendered page fragment using JBoss Cache. Note that
+               <literal>&lt;s:cache&gt;</literal> actually uses the instance
+               of JBoss Cache managed by the built-in 
+               <literal>pojoCache</literal> component.
+            </para>
+            <para><emphasis>Attributes</emphasis></para>
+            <itemizedlist>
+               <listitem>
+                  <para>
+                     <literal>key</literal> &#8212; the key to cache rendered 
+                     content, often a value expression. For example, if we were 
+                     caching a page fragment that displays a document, we might 
+                     use <literal>key="Document-#{document.id}"</literal>.
+                  </para>
+               </listitem>
+               <listitem>
+                  <para>
+                     <literal>enabled</literal> &#8212; a value expression that 
+                     determines if the cache should be used.
+                  </para>
+               </listitem>
+               <listitem>
+                  <para>
+                     <literal>region</literal> &#8212; a JBoss Cache node to use
+                     (different nodes can have different expiry policies).
+                 </para>
+               </listitem>
+            </itemizedlist>
+            
+            <para><emphasis>Usage</emphasis></para>
+            <programlisting role="XHTML"><![CDATA[<s:cache key="entry-#{blogEntry.id}" region="pageFragments">
+  <div class="blogEntry">
+    <h3>#{blogEntry.title}</h3>
+    <div>
+      <s:formattedText value="#{blogEntry.body}"/>
+    </div>
+    <p>
+      [Posted on&#160;
+      <h:outputText value="#{blogEntry.date}">
+        <f:convertDateTime timezone="#{blog.timeZone}" locale="#{blog.locale}" 
+                           type="both"/>
+      </h:outputText>]
+    </p>
+  </div>
+</s:cache>]]></programlisting>
+          
+         </section>
+
+
          <section>
             <title><literal>&lt;s:resource&gt;</literal></title>
  

Added: trunk/ui/src/main/java/org/jboss/seam/ui/ClientUidSelector.java
===================================================================
--- trunk/ui/src/main/java/org/jboss/seam/ui/ClientUidSelector.java	                        (rev 0)
+++ trunk/ui/src/main/java/org/jboss/seam/ui/ClientUidSelector.java	2009-04-02 22:52:19 UTC (rev 10279)
@@ -0,0 +1,58 @@
+package org.jboss.seam.ui;
+
+import javax.faces.context.FacesContext;
+
+import org.jboss.seam.annotations.Create;
+import org.jboss.seam.annotations.Name;
+import org.jboss.seam.faces.Selector;
+import org.jboss.seam.util.RandomStringUtils;
+
+/**
+ * <p>A selector which manages the cookie that gives the browser a
+ * unique identifier. This value is shared only between the browser
+ * and the server, thus allowing the server to determine if two
+ * distinct requests were made by the same source.</p>
+ * 
+ * <p>The identifier is stored in a cookie named <code>javax.faces.ClientToken</code>.</p>
+ * 
+ * @author Dan Allen
+ */
+ at Name("org.jboss.seam.ui.clientUidSelector")
+public class ClientUidSelector extends Selector
+{
+   private String clientUid;
+
+   @Create
+   public void onCreate()
+   {
+      setCookiePath(FacesContext.getCurrentInstance().getExternalContext().getRequestContextPath());
+      setCookieMaxAge(-1);
+      setCookieEnabled(true);
+      clientUid = getCookieValue();
+   }
+
+   public void seed()
+   {
+      if (!isSet()) {
+         clientUid = RandomStringUtils.randomAscii(50);
+         setCookieValueIfEnabled(clientUid);
+      }
+   }
+
+   public boolean isSet()
+   {
+      return clientUid != null;
+   }
+
+   public String getClientUid()
+   {
+      return clientUid;
+   }
+
+   @Override
+   protected String getCookieName()
+   {
+      return "javax.faces.ClientToken";
+   }
+
+}

Added: trunk/ui/src/main/java/org/jboss/seam/ui/UnauthorizedCommandException.java
===================================================================
--- trunk/ui/src/main/java/org/jboss/seam/ui/UnauthorizedCommandException.java	                        (rev 0)
+++ trunk/ui/src/main/java/org/jboss/seam/ui/UnauthorizedCommandException.java	2009-04-02 22:52:19 UTC (rev 10279)
@@ -0,0 +1,50 @@
+package org.jboss.seam.ui;
+
+import javax.faces.FacesException;
+
+/**
+ * An exception is thrown when the authenticity of a JSF command (i.e., form post)
+ * that relies on a UIToken cannot be verified.
+ * 
+ * @author Dan Allen
+ */
+public class UnauthorizedCommandException extends FacesException
+{
+   private String viewId;
+   
+   /**
+    * <p>Construct a new exception with no detail message or root cause.</p>
+    */
+   public UnauthorizedCommandException() {
+      super();
+   }
+   
+   /**
+    * <p>Construct a new exception with a detail message and the view ID</p>
+    */
+   public UnauthorizedCommandException(String viewId, String message) {
+      super(message);
+      this.viewId = viewId;
+   }
+
+   /**
+    * <p>Returns the view ID to which the authorized command was directed.</p>
+    */
+   public String getViewId()
+   {
+      return viewId;
+   }
+
+   /**
+    * <p>Returns the detail message explaining the reason for the denial.
+    * Includes the view ID if specified.</p>
+    */
+   @Override
+   public String getMessage()
+   {
+      if (viewId != null) {
+         return "viewId: " + viewId + " - " + super.getMessage();
+      }
+      return super.getMessage();
+   }
+}

Added: trunk/ui/src/main/java/org/jboss/seam/ui/component/UIToken.java
===================================================================
--- trunk/ui/src/main/java/org/jboss/seam/ui/component/UIToken.java	                        (rev 0)
+++ trunk/ui/src/main/java/org/jboss/seam/ui/component/UIToken.java	2009-04-02 22:52:19 UTC (rev 10279)
@@ -0,0 +1,110 @@
+package org.jboss.seam.ui.component;
+
+import javax.faces.component.UIForm;
+import javax.faces.component.UIOutput;
+
+import org.jboss.seam.Component;
+import org.jboss.seam.ui.ClientUidSelector;
+import org.jboss.seam.ui.UnauthorizedCommandException;
+
+/**
+ * <p>
+ * <strong>UIToken</strong> is a UIComponent that produces a random token that
+ * is inserted into a hidden form field to help to secure JSF form posts against
+ * cross-site request forgery (XSRF) attacks. This is an adaptation of the
+ * recommendation called Keyed‐Hashing for Message Authentication that is
+ * referenced in the Cross Site Reference Forgery by Jesse Burns
+ * (http://www.isecpartners.com/files/XSRF_Paper_0.pdf)
+ * </p>
+ * 
+ * <p>
+ * When placed inside a form, this component will first assign a unique
+ * identifier to the browser using a cookie that lives until the end of the
+ * browser session. This is roughly the browser's private key. Then a unique
+ * token is generated using various pieces of information that comprise the
+ * form's signature. The token may or may not be bound to the session id, as
+ * indicated by the value of the requireSession attribute. The token value is
+ * stored in the hidden form field named javax.faces.FormSignature.
+ * </p>
+ * 
+ * <p>
+ * There is an assumption when using this component that the browser supports
+ * cookies. Cookies are the only universally available persistent mechanism that
+ * can give the browser an identifiable signature. It's important to know that
+ * the browser submitting the form is the same browser that is requesting the
+ * form.
+ * </p>
+ * 
+ * <p>
+ * During the decode process, the token is generated using the same algorithm
+ * that was used during rendering and compared with the value of the request
+ * parameter javax.faces.FormSignature. If the same token value can be produced,
+ * then the form submission is permitted. Otherwise, an
+ * {@link UnauthorizedCommandException} is thrown indicating the reason for the
+ * failure.
+ * </p>
+ * 
+ * <p>
+ * The UIToken can be combined with client-side state saving or the
+ * "build before restore" strategy to unbind a POST from the session that
+ * created the view without sacrificing security. However, it's still the most
+ * secure to require the view state to be present in the session (JSF 1.2
+ * server-side state saving).
+ * </p>
+ * 
+ * <p>
+ * Please note that this solution isn't a complete panacea. If your site is
+ * vulnerable to XSS or the connection to wire-tapping, then the unique browser
+ * identifier can be revealed and a request forged.
+ * </p>
+ * 
+ * @author Dan Allen
+ */
+public abstract class UIToken extends UIOutput
+{
+   @SuppressWarnings("unused")
+   private static final String COMPONENT_TYPE = "org.jboss.seam.ui.Token";
+
+   @SuppressWarnings("unused")
+   private static final String COMPONENT_FAMILY = "org.jboss.seam.ui.Token";
+   
+   /**
+    * Indicates whether the session id should be included in the form signature,
+    * hence binding the token to the session. This value can be set to false
+    * if the "build before restore" mode of Facelets is activated (the
+    * default in JSF 2.0).
+    */
+   public abstract boolean isRequireSession();
+   
+   public abstract void setRequireSession(boolean required);
+   
+   /**
+    * Indicates whether a JavaScript check should be inserted into the page to
+    * verify that cookies are enabled in the browser. If cookies are not
+    * enabled, present a notice to the user that form posts will not work.
+    */
+   public abstract boolean isEnableCookieNotice();
+   
+   public abstract void setEnableCookieNotice(boolean state);
+   
+   /**
+    * Return the selector that controls the unique browser identifier cookie.
+    */
+   public ClientUidSelector getClientUidSelector() {
+      return (ClientUidSelector) Component.getInstance(ClientUidSelector.class);
+   }
+   
+   public String getClientUid() {
+      return getClientUidSelector().getClientUid();
+   }
+   
+   public UIForm getParentForm() {
+      while (getParent() != null) {
+         if (getParent() instanceof UIForm) {
+            return (UIForm) getParent();
+         }
+      }
+      
+      return null;
+   }
+}

Added: trunk/ui/src/main/java/org/jboss/seam/ui/renderkit/TokenRendererBase.java
===================================================================
--- trunk/ui/src/main/java/org/jboss/seam/ui/renderkit/TokenRendererBase.java	                        (rev 0)
+++ trunk/ui/src/main/java/org/jboss/seam/ui/renderkit/TokenRendererBase.java	2009-04-02 22:52:19 UTC (rev 10279)
@@ -0,0 +1,165 @@
+package org.jboss.seam.ui.renderkit;
+
+import java.io.IOException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import javax.faces.component.UIComponent;
+import javax.faces.component.UIForm;
+import javax.faces.context.FacesContext;
+import javax.faces.context.ResponseWriter;
+import javax.servlet.http.HttpSession;
+
+import org.jboss.seam.ui.ClientUidSelector;
+import org.jboss.seam.ui.UnauthorizedCommandException;
+import org.jboss.seam.ui.component.UIToken;
+import org.jboss.seam.ui.util.HTML;
+import org.jboss.seam.ui.util.cdk.RendererBase;
+import org.jboss.seam.util.Base64;
+import org.jboss.seam.util.RandomStringUtils;
+
+/**
+ * <p>
+ * The <strong>TokenRendererBase</strong> renders the form's signature as a
+ * hidden form field for the UIToken component.
+ * </p>
+ * 
+ * <p>
+ * The form signature is calculated as follows:
+ * </p>
+ * 
+ * <pre>
+ * sha1(signature = contextPath + viewId + &quot;,&quot; + formClientId + random alphanum, salt = clientUid)
+ * </pre>
+ * 
+ * <p>
+ * The developer can also choose to incorporate the session id into this hash to
+ * generate a more secure token (at the cost of binding it to the session) by
+ * setting the requireSession attribute to true. Then the calculation becomes:
+ * </p>
+ * 
+ * <pre>
+ * sha1(signature = contextPath + viewId + &quot;,&quot; + formClientId + &quot;,&quot; + random alphanum + sessionId, salt = clientUid)
+ * </pre>
+ * 
+ * <p>The decode method performs the following steps:</p>
+ * <ol>
+ * <li>check if this is a postback, otherwise skip the check</li>
+ * <li>check that this form was the one that was submitted, otherwise skip the check</li>
+ * <li>get the unique client identifier (from cookie), otherwise throw an exception that the browser must have unique identifier</li>
+ * <li>get the javax.faces.FormSignature request parameter, otherwise throw an exception that the form signature is missing</li>
+ * <li>generate the hash as before and verify that it equals the value of the javax.faces.FormSignature request parameter, otherwise throw an exception</li>
+ * </ol>
+ * 
+ * <p>If all of that passes, we are okay to process the form (advance to validate phase as decode() is called in apply request values).</p>
+ * 
+ * @author Dan Allen
+ * @see UnauthorizedCommandException
+ */
+public class TokenRendererBase extends RendererBase
+{
+   public static final String FORM_SIGNATURE_PARAM = "javax.faces.FormSignature";
+
+   public static final String RENDER_STAMP_ATTR = "javax.faces.RenderStamp";
+   
+   private static final String COOKIE_CHECK_SCRIPT_KEY = "org.jboss.seam.ui.COOKIE_CHECK_SCRIPT";
+
+   @Override
+   protected Class getComponentClass()
+   {
+      return UIToken.class;
+   }
+
+   @Override
+   protected void doDecode(FacesContext context, UIComponent component)
+   {
+      UIToken token = (UIToken) component;
+      UIForm form = token.getParentForm();
+      if (context.getRenderKit().getResponseStateManager().isPostback(context) && form.isSubmitted())
+      {
+         String clientToken = token.getClientUid();
+         String viewId = context.getViewRoot().getViewId();
+         if (clientToken == null)
+         {
+            throw new UnauthorizedCommandException(viewId, "No client identifier provided");
+         }
+
+         String requestedViewSig = context.getExternalContext().getRequestParameterMap().get(FORM_SIGNATURE_PARAM);
+         if (requestedViewSig == null)
+         {
+            throw new UnauthorizedCommandException(viewId, "No form signature provided");
+         }
+
+         if (!requestedViewSig.equals(generateViewSignature(context, form, token.isRequireSession(), clientToken)))
+         {
+            throw new UnauthorizedCommandException(viewId, "Form signature invalid");
+         }
+
+         form.getAttributes().remove(RENDER_STAMP_ATTR);
+      }
+   }
+
+   @Override
+   protected void doEncodeBegin(ResponseWriter writer, FacesContext context, UIComponent component) throws IOException
+   {
+      UIToken token = (UIToken) component;
+      UIForm form = token.getParentForm();
+      if (form == null)
+      {
+         throw new IllegalStateException("UIToken must be inside a UIForm.");
+      }
+      
+      writeCookieCheckScript(context, writer, token);
+
+      token.getClientUidSelector().seed();
+      form.getAttributes().put(RENDER_STAMP_ATTR, RandomStringUtils.randomAlphanumeric(50));
+      writer.startElement(HTML.INPUT_ELEM, component);
+      writer.writeAttribute(HTML.TYPE_ATTR, HTML.INPUT_TYPE_HIDDEN, HTML.TYPE_ATTR);
+      writer.writeAttribute(HTML.NAME_ATTR, FORM_SIGNATURE_PARAM, HTML.NAME_ATTR);
+      writer.writeAttribute(HTML.VALUE_ATTR, generateViewSignature(context, form, token.isRequireSession(), token.getClientUidSelector().getClientUid()), HTML.VALUE_ATTR);
+      writer.endElement(HTML.INPUT_ELEM);
+   }
+
+   /**
+    * If the client has not already delivered us a cookie and the cookie notice is enabled, write out JavaScript that will show the user
+    * an alert if cookies are not enabled.
+    */
+   private void writeCookieCheckScript(FacesContext context, ResponseWriter writer, UIToken token) throws IOException
+   {
+      if (!token.getClientUidSelector().isSet() && token.isEnableCookieNotice() && !context.getExternalContext().getRequestMap().containsKey(COOKIE_CHECK_SCRIPT_KEY)) {
+         writer.startElement(HTML.SCRIPT_ELEM, token);
+         writer.writeAttribute(HTML.TYPE_ATTR, "text/javascript", HTML.TYPE_ATTR);
+         writer.write("if (!document.cookie) {" +
+            " alert('This website uses a security measure that requires cookies to be enabled in your browser. Since you have cookies disabled, you will not be permitted to submit a form.');" +
+            " }");
+         writer.endElement(HTML.SCRIPT_ELEM);
+         context.getExternalContext().getRequestMap().put(COOKIE_CHECK_SCRIPT_KEY, true);
+      }
+   }
+
+   private String generateViewSignature(FacesContext context, UIForm form, boolean useSessionId, String saltPhrase)
+   {
+      String rawViewSignature = context.getExternalContext().getRequestContextPath() + "," + context.getViewRoot().getViewId() + "," + form.getClientId(context) + "," + form.getAttributes().get(RENDER_STAMP_ATTR);
+      if (useSessionId)
+      {
+         rawViewSignature += "," + ((HttpSession) context.getExternalContext().getSession(true)).getId();
+      }
+      try
+      {
+         MessageDigest digest = MessageDigest.getInstance("SHA-1");
+         digest.update(saltPhrase.getBytes());
+         byte[] salt = digest.digest();
+         digest.reset();
+         digest.update(rawViewSignature.getBytes());
+         digest.update(salt);
+         byte[] raw = digest.digest();
+         return Base64.encodeBytes(raw);
+      }
+      catch (NoSuchAlgorithmException ex)
+      {
+         ex.printStackTrace();
+         return null;
+      }
+   }
+   
+}




More information about the seam-commits mailing list