Security is and will always be a very important aspect of IT. Enterprise Content Management (or Document Management Systems or Content Services Platform or whateveryoucallit) is obviously not an exception to that. One of our main goals as a consultant is to add improvements to our customers’ installations for stability, security, and ease of management. However, it happens that not everything goes as planned… In this blog, I will discuss about a small but still annoying issue that appeared on D2 20.2 after enabling the Tomcat HTTP Security Headers at a customer.

D2 provides some capabilities to add some of the standard HTTP Security Headers via its own internal configuration. It already supports the HTTP Strict Transport Security (HSTS) and inside the “settings.properties” file, you can define some parameters like the HSTS max age (hsts.maxage=xxx). In this file, you can also configure D2 for anti-clickjacking (allowed.frame.origins=xxx). However, that’s not all the HTTP Security Headers, and you probably are using other applications besides D2 that are deployed on Tomcat in your company. Therefore, you might start looking at putting the configuration not on the application but instead on the Tomcat layer, to have a common configuration, enterprise wise.

Tomcat HTTP Security Headers (I’m only talking about “httpHeaderSecurity” filter here) doesn’t support everything but it’s still a good base so it might make sense to configure the headers at that layer. What is missing in the Tomcat HTTP Security Headers from the list of “common” Security Headers to define is mainly the Cache-Control, the Content-Security-Policy/CSP (here is a blog related to CSP config in regards to D2 WSCTF, if needed) and the Cross-Origin Resource Sharing/CORS but all of these can be configured through other filters (eithers OOTB filters or custom ones that you need to deploy). By default, the Tomcat HTTP Security Headers are disabled (commented):

[tomcat@d2-0 ~]$ web_xml="$TOMCAT_HOME/conf/web.xml"
[tomcat@d2-0 ~]$
[tomcat@d2-0 ~]$ grep -B2 -A4 'httpHeaderSecurity' ${web_xml}
<!--
    <filter>
        <filter-name>httpHeaderSecurity</filter-name>
        <filter-class>org.apache.catalina.filters.HttpHeaderSecurityFilter</filter-class>
        <async-supported>true</async-supported>
    </filter>
-->
--
<!--
    <filter-mapping>
        <filter-name>httpHeaderSecurity</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
    </filter-mapping>
-->
[tomcat@d2-0 ~]$

To enable it, you just need to uncomment the two sections (remove ‘<!–‘ before the XML tag and ‘–>‘ after) and you can of course start filling it with what you need. Here is a possible configuration:

[tomcat@d2-0 ~]$ cat ${web_xml}
...

    <filter>
        <filter-name>httpHeaderSecurity</filter-name>
        <filter-class>org.apache.catalina.filters.HttpHeaderSecurityFilter</filter-class>
        <async-supported>true</async-supported>
        <init-param>
            <param-name>hstsEnabled</param-name>
            <param-value>true</param-value>
        </init-param>
        <init-param>
            <param-name>hstsMaxAgeSeconds</param-name>
            <param-value>63072000</param-value>
        </init-param>
        <init-param>
            <param-name>hstsIncludeSubDomains</param-name>
            <param-value>true</param-value>
        </init-param>
        <init-param>
            <param-name>hstsPreload</param-name>
            <param-value>false</param-value>
        </init-param>
        <init-param>
            <param-name>antiClickJackingEnabled</param-name>
            <param-value>true</param-value>
        </init-param>
        <init-param>
            <param-name>antiClickJackingOption</param-name>
            <param-value>SAMEORIGIN</param-value>
        </init-param>
        <init-param>
            <param-name>blockContentTypeSniffingEnabled</param-name>
            <param-value>true</param-value>
        </init-param>
        <init-param>
            <param-name>xssProtectionEnabled</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>

...

    <filter-mapping>
        <filter-name>httpHeaderSecurity</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
    </filter-mapping>

...
[tomcat@d2-0 ~]$

The above is just an example but that’s how you enable the Tomcat HTTP Security Headers. Now coming to the issue I wanted to talk about in this blog… With these new headers in place, all applications (D2, D2-Config, D2-Smartview, D2-REST, DA, …) appeared to be working fine, no issues were reported by the tests done until someone tried to click on a link received by mail a few days before, and he got this beautiful screen:

Tomcat Red Warning

When that appeared, the following message was also printed on the Tomcat logs:

2022-03-03 13:56:52,342 UTC SEVERE [https-jsse-nio-8080-exec-27] org.apache.catalina.core.StandardWrapperValve.invoke Servlet.service() for servlet [jsp] in context with path [/D2] threw exception [Unable to add HTTP headers since response is already committed on entry to the HTTP header security Filter] with root cause
	javax.servlet.ServletException: Unable to add HTTP headers since response is already committed on entry to the HTTP header security Filter
		at org.apache.catalina.filters.HttpHeaderSecurityFilter.doFilter(HttpHeaderSecurityFilter.java:101)
		at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
		at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
		at com.emc.x3.portal.server.filters.HttpHeaderFilter.doFilter(HttpHeaderFilter.java:87)
		at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
		at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
		at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:61)
		at org.apache.shiro.web.servlet.AdviceFilter.executeChain(AdviceFilter.java:108)
		at com.emc.x3.portal.server.filters.authc.X3SAMLHttpAuthenticationFilter.executeChain(X3SAMLHttpAuthenticationFilter.java:356)
		at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:137)
		at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
		at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66)
		at org.apache.shiro.web.servlet.AbstractShiroFilter.executeChain(AbstractShiroFilter.java:449)
		at org.apache.shiro.web.servlet.AbstractShiroFilter$1.call(AbstractShiroFilter.java:365)
		at org.apache.shiro.subject.support.SubjectCallable.doCall(SubjectCallable.java:90)
		at org.apache.shiro.subject.support.SubjectCallable.call(SubjectCallable.java:83)
		at org.apache.shiro.subject.support.DelegatingSubject.execute(DelegatingSubject.java:387)
		at org.apache.shiro.web.servlet.AbstractShiroFilter.doFilterInternal(AbstractShiroFilter.java:362)
		at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
		at com.google.inject.servlet.ManagedFilterPipeline.dispatch(ManagedFilterPipeline.java:121)
		at com.google.inject.servlet.GuiceFilter.doFilter(GuiceFilter.java:133)
		at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
		at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
		at com.emc.x3.portal.server.filters.X3SessionTimeoutFilter.doFilter(X3SessionTimeoutFilter.java:40)
		at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
		at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
		at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197)
		at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
		at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:540)
		at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135)
		at org.apache.catalina.valves.StuckThreadDetectionValve.invoke(StuckThreadDetectionValve.java:206)
		at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
		at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:687)
		at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
		at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357)
		at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:382)
		at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
		at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:895)
		at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1722)
		at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
		at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
		at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
		at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
		at java.base/java.lang.Thread.run(Thread.java:829)

As you can see, it seems to be linked to what was just configured (“HttpHeaderSecurityFilter” on the first line of the stack trace) and there is also quite a bit of “shiro” references as well as a “X3SAMLHttpAuthenticationFilter“. It would therefore indicate that the issue is most probably linked to the Single Sign-On in relation with the Tomcat HTTP Security Headers. Since Tomcat is unable to add the required HTTP Security Headers, then it fails the request and print the red warning page.

What happened is that the link on the mail contained URL parameters (i.e., the “query” part) so it was something like “https://dns/D2?docbase=xxx&locateId=xxx“. D2 was fully working properly, except when someone tried to directly open a link from a mail without opening D2 first, which shouldn’t be a problem under normal circumstances… When that happened, the user was redirected to the Single Sign-On (AzureAD SAML2, configured on the shiro.ini), and then back to D2, which was supposed to handle the user’s request but failed because of the above Tomcat error. In addition to the issue (and the nice red warning screen), the URL changed to “https://dns/D2” (i.e., the URL parameters were lost in the process).

However, clicking a second time on the link from the mail worked properly and displayed the document correctly (using the “locateId” parameter)! The easiest way to replicate the issue was therefore to clear the cookies and access any URL with parameters. It appeared to me that, the issue would only show up if there is no SAML cookies (SAMLNameID, SAMLSessionIndex). After disabling the SSO (i.e., renaming/removing the “shiro.ini” file and restarting Tomcat), the issue wasn’t reproducible anymore. This confirmed my assumptions that there is a conflict between how D2 handles the SAML response and how Tomcat adds the HTTP Security Headers (“response is already committed“).

With that in mind, I opened the OpenText SR#5056234, to see if it was a D2 bug or something else. After some more investigations, I got the information from OpenText that this is apparently happening because of the open source third party library that is used for SSO (i.e., Apache Shiro). Without Apache Shiro, the HTTP Security Headers are properly added/modified by the doFilter() function before the servlet action is done. However, with Apache Shiro (that chains additional filters), the order of filters execution appears to be changed and, in this case, the response is already committed before Tomcat has the chance to add the necessary HTTP Security Headers, which triggers this error. As far as I know, this issue doesn’t happen with OTDS SSO, so I assume different libraries are in play there.

Since this issue can be seen as soon as the Tomcat HTTP Security Headers are enabled (even if you don’t specify any configuration for the filter), then it kind of makes it difficult to setup it up at the Tomcat layer. D2 20.2 uses Apache Shiro 1.4.2 (Nov-2019) while the latest version, as of today, is 1.9.1 (Jun-2022). I didn’t check but this issue might have been solved in a later version. Therefore, potential solutions could be to update the libraries (knowing that it could bring issues as well…) or simply to NOT configure the HTTP Security Headers on Tomcat, but instead put them one level above. If you are using Tomcat, there is a good chance that you have a front-end on top of it like Apache HTTPD or Nginx. The issue being related to Tomcat servlet handling, configuring/adding the HTTP Security Headers upfront will prevent the issue to appear altogether, hence that would most probably be the preferred solution.