javaspringurlencodehtml-escape-characterstuckey-urlrewrite-filter

When a browser opens a URL with params it hangs and Java App (using UrlRewrite) throws RequestRejectedException: the URL contained ";"


There is a Java, Spring, Tomcat webapp. One part is responsible for sending activation emails to the newly created users with generated activationUrls (consisting of unique tokens). The email is created from a Freemarker template, where the activationUrl gets substituted before email is sent to the user. Once the user opens activation URL in a browser it should load (redirect to) the user activation page where they need to provide a password, etc.

Issue is, that the browser hangs with "Processing..." and nothing is happening in the frontend. In the backend we can see this stacktrace:

13-Dec-2023 15:48:17.595 SEVERE [http-nio-8080-exec-9] org.apache.catalina.core.StandardWrapperValve.invoke Servlet.service() for servlet [default] in context with path [/ui] threw exception
    org.springframework.security.web.firewall.RequestRejectedException: The request was rejected because the URL contained a potentially malicious String ";"
        at org.springframework.security.web.firewall.StrictHttpFirewall.rejectedBlacklistedUrls(StrictHttpFirewall.java:288)
        at org.springframework.security.web.firewall.StrictHttpFirewall.getFirewalledRequest(StrictHttpFirewall.java:267)
        at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:193)
        at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:177)
        at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
        at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153)
        at org.tuckey.web.filters.urlrewrite.RuleChain.handleRewrite(RuleChain.java:176)
        at org.tuckey.web.filters.urlrewrite.RuleChain.doRules(RuleChain.java:145)
        at org.tuckey.web.filters.urlrewrite.UrlRewriter.processRequest(UrlRewriter.java:92)
        at org.tuckey.web.filters.urlrewrite.UrlRewriteFilter.doFilter(UrlRewriteFilter.java:394)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153)
        at org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter.doFilterInternal(OpenEntityManagerInViewFilter.java:178)

The activationUrl which is genereted and received via email looks like: http://localhost:8080/ui/activate-account?tokenId=3&token=1462 (it's already simplified, but the key bit is, it has two url parameters separated by ('&') ampersand char, which is getting "translated" into '&' which contains that potentially malicious String ";".

We use tuckey.org UrlRewrite 4.0.3

We do have specified a rule for that:

<rule>
    <name>RULE: redirect security urls to index</name>
    <from>/(forgot-password|forgot-username|activate-account|reset-password|signout|locked-account|sso-error|ird-access-token)([\?\&amp;\=a-z0-9-]*)</from>
    <to last="true">/security/index.zul?function=$1</to>
</rule>

Please note, the way how the ampersand (url parameter separator) is represented in the rule.

My suspicious is that processing the rule is causing that ?param1=xxxx&param2=yyyy is converted into ?param1=xxxx&amp;param2=yyyy which is causing that exception to be thrown.

From the urlrewrite manual, I am not able to figure out any sort of attributes which I could apply on the to element. Something like escapeBackReference (whether the back-references in the rewritten URL should be escaped or not) or encodeUrl attribute which would specify whether the rewritten URL should be encoded or not.

Any idea how to resolve it?

FYI: When reload page is triggered in a browser, then the page is reloaded and URL is processed and a correct activation page is displayed. This can be reliably reproduce by pasting the URL into a private/incognito window of any browser (Edge, Chrome, FF). Repeated attempts from a normal browser session or even from the same incognito mode won't lead to this issue and will work as expected. However, each new user will have a fresh "new" browser before not connected to our servers, so each of them is going to experience it.


Solution

  • Further investigation

    Debugging through Java code of external libraries, I noticed that in this method: org.springframework.security.web.firewall.StrictHttpFirewall.rejectedBlacklistedUrls(HttpServletRequest request) where encodedUrlContains(...) is called, they are using two queries on the HttpServletRequest:

    where value is e.g. ";" , but

    request.getContextPath() is /app-ui request.getRequestURI() is /app-ui/zkau/web/db026fbc/js/zk.wpd;jsessionid=C748DE1595B19CCFE6370FB096DF7942 and request.getRequestURL() is http://localhost:8080/app-ui/zkau/web/db026fbc/js/zk.wpd;jsessionid=C748DE1595B19CCFE6370FB096DF7942 where the ";" semicolon is present.

    Actually, whilst I thought a browser supposed to process a single URL, StrictHttpFirewall.getFirewalledRequest(HttpServletRequest request) processes multiple times different requests with various values obtained from request.getRequestURI(), eg:

    But the situation of attaching ";" with jsessionid=... parameter is happening only in a browser private mode (or when the browser doesn’t have stored any session info in the cookies).

    So, the issue doesn’t lie in the activation-url, but somewhere else where we handle the session info.

    Using Browser Dev Tools

    Looking into a browser developers tools confirms that a session info is attached to the URL (if it is a 1st time visit) enter image description here Please note the Status, Indicator and Name (hoover a mouse over it to see a full URL with the ";" in there separating a URL of some kind of an App object from jsessionid attribute).

    If a reload is hit (or pretty much the activate account URL is opened in a browser which “remembers” it was connecting to the App-UI app before), then no jsessionid attribute is added after the URLs, see below: enter image description here All statuses are 200, no ";" and jsessionid in the URLs.

    Conclusion

    So, it turned out to be the cause with the jsessionid. It seems that it will be sent on the url for the first request as it does not know if the browser supports cookies yet. To prevent this we had to add the following to the bottom of the webapp/WEB-INF/web.xml in the app-ui

    <session-config>
        <tracking-mode>COOKIE</tracking-mode>
        <cookie-config>
            <http-only>true</http-only>
            <secure>true</secure>
        </cookie-config>
    </session-config>
    

    This definitely had resolved the issue for new users who hadn't visited our app before.