Uploaded image for project: 'Confluence Data Center'
  1. Confluence Data Center
  2. CONFSERVER-25388

Redirect after login does not compare destination URL to Server Base URL

    • Icon: Bug Bug
    • Resolution: Resolved Locally
    • Icon: Low Low
    • None
    • 4.1.9, 4.2.4, 4.3.2
    • None
    • Confluence listening on localhost over HTTP, Apache reverse-proxy listening to the world over HTTPS.

      Summary

      There is an issue with logging into a protected page when Confluence is being served behind an Apache proxy where the public-facing URL is over SSL (https://) and confluence is listening over non-SSL (http://). In this example, Apache is listening on port 443 and proxies requests to http://localhost:8090 for Confluence to serve. Also, in this example, the Confluence Server Base URL is set correctly to the public-facing URL "https://confluence...".

      The issue is that when a protected page redirects to the login page, it sets the os_destination variable to an absolute path of "https://confluence.../path/to/destination/page". After a successful login, Confluence will check the "context" of the destination to see if it's secure to redirect to the URL.

      The Problem

      The issue is that the code for checking this context is incorrect. It builds a URL based on the incoming request and then compares it to the destination URL. The problem in this case is that the incoming request is over HTTP, since it is proxied by Apache. And so the URL that Confluence builds is of the type: "http://confluence.../path/to/destination/page", which when compared to the destination URL of "https://confluence.../path/to/destination", is obviously incorrect.

      A point to consider that validates this claim is that if the os_destination variable is manually set to "http://confluence.../path/to/destination", Confluence will redirect to this page correctly after login.

      The Problem in Code

      Now, getting to actual code, I've gone through the Confluence source and found out where the problem lies and have come up with a possible solution to the problem which uses the Confluence Base URL setting to build the comparison URL instead of using the request object. I have not tested the solution code yet, but I will do so shortly. (Solution code is at the bottom.)

      Now, the actual problem, I believe, is in how "allowedRedirectDestination()" performs its checks to determine if the suggested destination URL is "valid." allowedRedirectDestination() is implemented in DefaultRedirectPolicy.java:

      DefaultRedirectPolicy.java
          public boolean allowedRedirectDestination(final String redirectUrl, final HttpServletRequest request)
          {
              // Test for total trust
              if (allowAnyUrl)
              {
                  return true;
              }
              // Otherwise we use default behaviour: allow valid redirects to the same context.
              URI uri;
              try
              {
                  // Attempt to parse the URI
                  uri = new URI(redirectUrl);
              }
              catch (final URISyntaxException e)
              {
                  // Invalid URI - not allowed. This stops possible header injection attacks (see SER-127)
                  // but it is also good in general that if we can't parse a URI, then we can't trust it.
                  return false;
              }
              // The URI is valid - if it is absolute, then check that it is to the same context
              return !uri.isAbsolute() || RedirectUtils.sameContext(redirectUrl, request);
          }
      

      A workaround that was suggested enables the "allowAnyUrl" option, which we do not want to do for security reasons. This method ultimately comes down to the call to "RedirectUtils.sameContext(redirectUrl, request)", which is implemented in RedirectUtils.java:

      RedirectUtils.java
          public static boolean sameContext(final String url, final HttpServletRequest request)
          {
              // build up the context from the request
              String context = getServerNameAndPath(request, false);
              if (sameContext(url, context))
              {
                  return true;
              }
              // Its possible that the requested URL contains an explicit port number even though it is not required. Check for this.
              context = getServerNameAndPath(request, true);
              return sameContext(url, context);
          }
          private static boolean sameContext(final String url, String requestContext)
          {
              // Now, if the incoming context is "/jira", we want "/jira" and "/jira/whatever" to be considered the same context
              // but not "/jiranot"
              if (url.equals(requestContext))
              {
                  return true;
              }
              // http://java.sun.com/javaee/5/docs/api/javax/servlet/ServletContext.html#getContextPath()
              // Note that Context path should not include a trailing '/', but we will be careful anyway
              if (!requestContext.endsWith("/"))
              {
                  requestContext = requestContext + '/';
              }
              return url.startsWith(requestContext);
          }
      

      This calls "getServerNameAndPath(request, false)", which builds a string to compare the url to in order to determine if it is "in the same context".

      RedirectUtils.java
          private static String getServerNameAndPath(HttpServletRequest request, boolean showDefaultPortNumber)
          {
              StringBuffer buf = new StringBuffer();
              buf.append(request.getScheme()).
                      append("://").
                      append(request.getServerName());
              if (showDefaultPortNumber ||
                  ("http".equals(request.getScheme()) && request.getServerPort() != 80) || 
                  ("https".equals(request.getScheme()) && request.getServerPort() != 443)
                  )
              {
                  buf.append(":").append(request.getServerPort());
              }
              buf.append(request.getContextPath());
              return buf.toString();
          }
      

      I believe the problem is in this method. It calls "request.getScheme()", which in our scenario would be "HTTP" since confluence is listening over HTTP to the Apache process. In our scenario, the result of "getServerNameAndPath(request, false)" would be "http://confluence.../path/to/destination", which when compared to the os_destination variable in "sameContext(final String url, String requestContext)", would return false:

      RedirectUtils.java
              return url.startsWith(requestContext);
      

      This also explains why changing the destination URL to "http://confluence.../path/to/destination" works as expected.

      Solution

      A solution that I believe would work correctly would be to add an extra check to "sameContext(final String url, final HttpServletRequest request)" to see if the URL starts with the Confluence Base URL, and if it does, strip that off the front so we only compare the rest of the path for "same context."

      RedirectUtils.java - Modified
          public static boolean sameContext(final String url, final HttpServletRequest request)
          {
              // Get Confluence Base URL
              String baseUrl = getConfluenceConfig().getBaseUrl(); // pseudo-code
              URI testURI;
              try
              {
                  testURI = new URI(baseUrl);
              }
              catch (final URISyntaxException e)
              {
                  // Base URL is not parseable...
              }
              String baseSchemeAndHost = testURI.getScheme() + "://" + testURI.getHost(); // might need to account for port too
              StringBuilder testUrl = new StringBuilder(url);
              if (testUrl.indexOf(baseSchemeAndHost) == 0)
              {
                  // Remove Base URL Scheme and Host from the front of the URL
                  testUrl.delete(0, baseSchemeAndHost.length());
              }
              StringBuilder contextUrl = request.getRequestURL();
              if (contextUrl.indexOf(baseSchemeAndHost) == 0)
              {
                  // Remove Base URL Scheme and Host from the front of the URL
                  contextUrl.delete(0, baseSchemeAndHost.length());
              }
              if (sameContext(testUrl.toString(), contextUrl.toString()))
              {
                  return true;
              }
      
              // build up the context from the request
              String context = getServerNameAndPath(request, false);
              if (sameContext(url, context))
              {
                  return true;
              }
              // Its possible that the requested URL contains an explicit port number even though it is not required. Check for this.
              context = getServerNameAndPath(request, true);
              return sameContext(url, context);
          }
      

            [CONFSERVER-25388] Redirect after login does not compare destination URL to Server Base URL

              shaffenden Steve Haffenden (Inactive)
              c19160f8b72e Brandon Carl
              Affected customers:
              1 This affects my team
              Watchers:
              3 Start watching this issue

                Created:
                Updated:
                Resolved: