View Javadoc

1   package com.atlassian.xwork.interceptors;
2   
3   import com.opensymphony.xwork.interceptor.Interceptor;
4   import com.opensymphony.xwork.ActionInvocation;
5   import com.opensymphony.xwork.ActionSupport;
6   import com.opensymphony.xwork.Action;
7   import com.opensymphony.xwork.ValidationAware;
8   import com.opensymphony.webwork.ServletActionContext;
9   import com.atlassian.xwork.RequireSecurityToken;
10  import com.atlassian.xwork.SimpleXsrfTokenGenerator;
11  import com.atlassian.xwork.XsrfTokenGenerator;
12  import com.atlassian.xwork.XWorkVersionSupport;
13  
14  import java.lang.reflect.Method;
15  
16  /**
17   * Interceptor to add XSRF token protection to XWork actions. Configuring XSRF protection happens at the method
18   * level, and can be done either by adding a @RequireSecurityToken annotation to the method, or by adding a
19   * <param name="RequireSecurityToken">[true|false]</param> parameter to the action configuration in
20   * <code>xwork.xml</code>.
21   *
22   * <p>Configuration in xwork.xml will override any annotation-based configuration. Behaviour when a method is
23   * not configured at all depends on the SecurityLevel seeting
24   *
25   * <p>Requests containing the HTTP header <code>X-Atlassian-Token: no-check</code> will bypass the check and always
26   * succeed.
27   *
28   * @see SecurityLevel
29   * @see #getSecurityLevel()
30   *
31   * TODO: Make this work with the RestrictHttpMethodInterceptor so get-only methods are not protected?
32   */
33  public class XsrfTokenInterceptor implements Interceptor
34  {
35      public static final String REQUEST_PARAM_NAME = "atl_token";
36      public static final String CONFIG_PARAM_NAME = "RequireSecurityToken";
37      public static final String VALIDATION_FAILED_ERROR_KEY = "atlassian.xwork.xsrf.badtoken";
38      public static final String SECURITY_TOKEN_REQUIRED_ERROR_KEY = "atlassian.xwork.xsrf.notoken";
39      public static final String OVERRIDE_HEADER_NAME = "X-Atlassian-Token";
40      public static final String OVERRIDE_HEADER_VALUE = "no-check";
41  
42      public static enum SecurityLevel
43      {
44          /** Methods without any configuration are not protected by default */
45          OPT_IN(false),
46          /** Methods without any configuration are protected by default */
47          OPT_OUT(true);
48  
49          private final boolean defaultProtection;
50  
51          SecurityLevel(boolean defaultProtection)
52          {
53              this.defaultProtection = defaultProtection;
54          }
55  
56          public boolean getDefaultProtection()
57          {
58              return defaultProtection;
59          }
60      }
61  
62      private final XsrfTokenGenerator tokenGenerator;
63      private final XWorkVersionSupport versionSupport;
64  
65      public XsrfTokenInterceptor(XWorkVersionSupport versionSupport)
66      {
67          this(new SimpleXsrfTokenGenerator(), versionSupport);
68      }
69  
70      public XsrfTokenInterceptor(XsrfTokenGenerator tokenGenerator, XWorkVersionSupport versionSupport)
71      {
72          this.tokenGenerator = tokenGenerator;
73          this.versionSupport = versionSupport;
74      }
75  
76      public String intercept(ActionInvocation invocation) throws Exception
77      {
78          Method invocationMethod = versionSupport.extractMethod(invocation);
79          String configParam = (String) invocation.getProxy().getConfig().getParams().get(CONFIG_PARAM_NAME);
80          RequireSecurityToken annotation = invocationMethod.getAnnotation(RequireSecurityToken.class);
81  
82          boolean isProtected = methodRequiresProtection(configParam, annotation);
83          String token = ServletActionContext.getRequest().getParameter(REQUEST_PARAM_NAME);
84          boolean validToken = tokenGenerator.validateToken(ServletActionContext.getRequest(), token);
85  
86          if (isProtected && !validToken)
87          {
88              if (token == null)
89              {
90                  addInvalidTokenError(versionSupport.extractAction(invocation), SECURITY_TOKEN_REQUIRED_ERROR_KEY);
91              } 
92              else 
93              {
94                  addInvalidTokenError(versionSupport.extractAction(invocation), VALIDATION_FAILED_ERROR_KEY);
95              }
96              ServletActionContext.getResponse().setStatus(403);
97              return ActionSupport.INPUT;
98          }
99  
100         return invocation.invoke();
101     }
102 
103     private boolean methodRequiresProtection(String configParam, RequireSecurityToken annotation)
104     {
105         if (isOverrideHeaderPresent())
106             return false;
107         if (configParam != null)
108             return Boolean.valueOf(configParam);
109         else if (annotation != null)
110             return annotation.value();
111         else
112             return getSecurityLevel().getDefaultProtection();
113     }
114 
115     /**
116      * Add error to action in cases where token is required, but is missing or invalid. Implementations may
117      * wish to override this method, but most should be able to get away with just overriding
118      * {@link #internationaliseErrorMessage}
119      *
120      * @param action the action to add the error message to
121      * @param errorMessageKey the error message key that will be used to internationalise the message 
122      */
123     protected void addInvalidTokenError(Action action, String errorMessageKey)
124     {
125         if (action instanceof ValidationAware)
126             ((ValidationAware)action).addActionError(internationaliseErrorMessage(action, errorMessageKey));
127     }
128 
129     /**
130      * Convert an error message key into the correct message for the current user's locale. The default implementation
131      * is only useful for testing. Implementations should override this method to provide the appropriate
132      * internationalised implementation.
133      *
134      * @param action the current action being executed
135      * @param messageKey the message key that needs internationalising
136      * @return the appropriate internationalised message for the current user
137      */
138     protected String internationaliseErrorMessage(Action action, String messageKey)
139     {
140         return messageKey;
141     }
142 
143     private boolean isOverrideHeaderPresent()
144     {
145         return OVERRIDE_HEADER_VALUE.equals(ServletActionContext.getRequest().getHeader(OVERRIDE_HEADER_NAME));
146     }
147 
148     ///CLOVER:OFF
149 
150     public void destroy()
151     {
152     }
153 
154     public void init()
155     {
156     }
157 
158     /**
159      * Gets the current security level. See {@link SecurityLevel} for more information on the meanings of the different
160      * level. Default implementation returns <code>OPT_IN</code>. Implementations should override this method if they
161      * want more control over the security level setting.
162      *
163      * @return the security level to apply to this interceptor.
164      */
165     protected SecurityLevel getSecurityLevel()
166     {
167         return SecurityLevel.OPT_IN;
168     }
169 }