CVE-2014-0120: Cross-Site Request Forgery (CSRF) (CWE-352)
value: Cross-site request forgery (CSRF) vulnerability in the admin terminal in Hawt.io allows remote attackers to hijack the authentication of arbitrary users for requests that run commands on the Karaf server, as demonstrated by running "shutdown -f."
CWE description: The web application does not, or can not, sufficiently verify whether a well-formed, valid, consistent request was intentionally provided by the user who submitted the request.
CVSS v3 base score: 8.8
Published: 2017-12-29T22:29Z
Repository: https://github.com/hawtio/hawtio
Language: Java
Files changed: hawtio-karaf-terminal/src/main/java/io/hawt/web/plugin/karaf/terminal/TerminalServlet.java, hawtio-karaf-terminal/src/main/webapp/WEB-INF/web.xml, hawtio-karaf-terminal/src/main/webapp/app/js/gogo.js, hawtio-karaf-terminal/src/main/webapp/app/js/gogoPlugin.js, hawtio-system/src/main/java/io/hawt/web/LoginServlet.java, hawtio-system/src/main/java/io/hawt/web/LoginTokenServlet.java
Commit date: 2014-03-06 12:11:42 -0500
Commit message: Add a LoginTokenServlet that plugins can use to fetch a token, and let's use it in hawtio-karaf-terminal.  Also handle cases where the terminal scope gets created a couple times.

Diff: diff --git a/hawtio-karaf-terminal/src/main/java/io/hawt/web/plugin/karaf/terminal/TerminalServlet.java b/hawtio-karaf-terminal/src/main/java/io/hawt/web/plugin/karaf/terminal/TerminalServlet.java
index 1e8b872e..414ab1e5 100644
--- a/hawtio-karaf-terminal/src/main/java/io/hawt/web/plugin/karaf/terminal/TerminalServlet.java
+++ b/hawtio-karaf-terminal/src/main/java/io/hawt/web/plugin/karaf/terminal/TerminalServlet.java
@@ -1,25 +1,23 @@
 package io.hawt.web.plugin.karaf.terminal;
 
 import io.hawt.system.Helpers;
+import io.hawt.web.LoginTokenServlet;
 import org.apache.felix.service.command.CommandProcessor;
 import org.apache.felix.service.command.CommandSession;
 import org.apache.felix.service.threadio.ThreadIO;
 import org.apache.karaf.shell.console.jline.Console;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import javax.security.auth.Subject;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpSession;
 import java.io.*;
 import java.lang.reflect.Constructor;
-import java.security.AccessControlContext;
-import java.security.AccessController;
 import java.util.zip.GZIPOutputStream;
 
 /**
  *
  */
@@ -43,26 +41,21 @@ public class TerminalServlet extends HttpServlet {
 
     @Override
     protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
 
         HttpSession session = request.getSession(false);
-        if (session == null) {
-          AccessControlContext acc = AccessController.getContext();
-          Subject subject = Subject.getSubject(acc);
-          if (subject == null) {
+        String token = request.getHeader(LoginTokenServlet.LOGIN_TOKEN);
+
+        if (token == null || session == null) {
             Helpers.doForbidden(response);
             return;
-          }
-          session = request.getSession(true);
-          session.setAttribute("subject", subject);
-        } else {
-          Subject subject = (Subject) session.getAttribute("subject");
-          if (subject == null) {
-            session.invalidate();
+        }
+
+        String sessionToken = (String) session.getAttribute(LoginTokenServlet.LOGIN_TOKEN);
+        if (!token.equals(sessionToken)) {
             Helpers.doForbidden(response);
             return;
-          }
         }
 
         String encoding = request.getHeader("Accept-Encoding");
         boolean supportsGzip = (encoding != null && encoding.toLowerCase().indexOf("gzip") > -1);
         SessionTerminal st = (SessionTerminal) session.getAttribute("terminal");
diff --git a/hawtio-karaf-terminal/src/main/webapp/WEB-INF/web.xml b/hawtio-karaf-terminal/src/main/webapp/WEB-INF/web.xml
index d4b5e775..465f3e7a 100644
--- a/hawtio-karaf-terminal/src/main/webapp/WEB-INF/web.xml
+++ b/hawtio-karaf-terminal/src/main/webapp/WEB-INF/web.xml
@@ -14,11 +14,32 @@
   </filter>
   <filter-mapping>
     <filter-name>AuthenticationFilter</filter-name>
     <url-pattern>/term/*</url-pattern>
   </filter-mapping>
+  <filter-mapping>
+    <filter-name>AuthenticationFilter</filter-name>
+    <url-pattern>/auth/*</url-pattern>
+  </filter-mapping>
+
+  <servlet>
+    <servlet-name>login</servlet-name>
+    <servlet-class>io.hawt.web.LoginTokenServlet</servlet-class>
+  </servlet>
+  <servlet-mapping>
+    <servlet-name>login</servlet-name>
+    <url-pattern>/auth/login/*</url-pattern>
+  </servlet-mapping>
 
+  <servlet>
+    <servlet-name>logout</servlet-name>
+    <servlet-class>io.hawt.web.LogoutServlet</servlet-class>
+  </servlet>
+  <servlet-mapping>
+    <servlet-name>logout</servlet-name>
+    <url-pattern>/auth/logout/*</url-pattern>
+  </servlet-mapping>
 
   <servlet>
     <servlet-name>TerminalServlet</servlet-name>
     <servlet-class>io.hawt.web.plugin.karaf.terminal.TerminalServlet</servlet-class>
     <load-on-startup>1</load-on-startup>
diff --git a/hawtio-karaf-terminal/src/main/webapp/app/js/gogo.js b/hawtio-karaf-terminal/src/main/webapp/app/js/gogo.js
index 210110c0..75d25b39 100644
--- a/hawtio-karaf-terminal/src/main/webapp/app/js/gogo.js
+++ b/hawtio-karaf-terminal/src/main/webapp/app/js/gogo.js
@@ -4,11 +4,11 @@
 //  Public Domain License
 //
 
 gogo = { };
 
-gogo.Terminal_ctor = function(div, width, height, authHeader) {
+gogo.Terminal_ctor = function(div, width, height, token) {
 
    var query0 = "w=" + width + "&h=" + height;
    var query1 = query0 + "&k=";
    var buf = "";
    var timeout;
@@ -45,11 +45,11 @@ gogo.Terminal_ctor = function(div, width, height, authHeader) {
            if (force) {
                query = query + "&f=1";
                force = 0;
            }
            r.open("POST", "hawtio-karaf-terminal/term", true);
-           r.setRequestHeader('Authorization', authHeader);
+           r.setRequestHeader('LoginToken', token);
            r.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
            r.onreadystatechange = function () {
                if (r.readyState == 4) {
                    if (r.status == 200) {
                        window.clearTimeout(error_timeout);
@@ -222,9 +222,9 @@ gogo.Terminal_ctor = function(div, width, height, authHeader) {
 
    init();
 
 }
 
-gogo.Terminal = function(div, width, height, authHeader) {
-   return new this.Terminal_ctor(div, width, height, authHeader);
+gogo.Terminal = function(div, width, height, token) {
+   return new this.Terminal_ctor(div, width, height, token);
 }
 
diff --git a/hawtio-karaf-terminal/src/main/webapp/app/js/gogoPlugin.js b/hawtio-karaf-terminal/src/main/webapp/app/js/gogoPlugin.js
index f323dbeb..0c53f2b9 100644
--- a/hawtio-karaf-terminal/src/main/webapp/app/js/gogoPlugin.js
+++ b/hawtio-karaf-terminal/src/main/webapp/app/js/gogoPlugin.js
@@ -14,10 +14,38 @@ var Gogo = (function() {
     }).directive('gogoTerminal', function(log, userDetails) {
       return {
         restrict: 'A',
         link: function(scope, element, attrs) {
 
+          scope.$on("$destroy", function(e) {
+            scope.destroyed = true;
+            document.onkeypress = null;
+            document.onkeydown = null;
+            if (!('term' in scope)) {
+              return;
+            }
+            var url = "hawtio-karaf-terminal/auth/logout/";
+            delete scope.term;
+            $.ajax(url, {
+              type: "POST",
+              success: function (response) {
+                log.debug("logged out of terminal");
+                Core.$apply(scope);
+              },
+              error: function (xhr, textStatus, error) {
+                log.info("Failed to log out of terminal: ", error);
+              },
+              beforeSend: function (xhr) {
+                xhr.setRequestHeader('Authorization', authHeader);
+              }
+            })
+          });
+
+          if (scope.destroyed) {
+            return;
+          }
+
           var width = 120;
           var height = 39;
 
           var div = $('<div class="terminal">A</div>').css({
             position: 'absolute',
@@ -50,15 +78,36 @@ var Gogo = (function() {
             'min-height': cssHeight
           });
 
           var authHeader = Core.getBasicAuthHeader(userDetails.username, userDetails.password);
 
-          gogo.Terminal(element.get(0), width, height, authHeader);
-
-          scope.$on("$destroy", function(e) {
-            document.onkeypress = null;
-            document.onkeydown = null;
+          var url = "hawtio-karaf-terminal/auth/login/";
+
+          $.ajax(url, {
+            type: "POST",
+            success: function (response) {
+              if (scope.destroyed) {
+                log.debug("Scope's been destroyed since we made our request, let's not create a terminal instance");
+                return;
+              }
+              log.debug("got back response: ", response);
+              if ('term' in scope) {
+                log.debug("Previous terminal created, let's clean it up");
+                document.onkeypress = null;
+                document.onkeydown = null;
+                delete scope.term;
+              }
+              scope.term = gogo.Terminal(element.get(0), width, height, response['token']);
+              Core.$apply(scope);
+
+            },
+            error: function (xhr, textStatus, error) {
+              log.warn("Failed to log into terminal: ", error);
+            },
+            beforeSend: function (xhr) {
+              xhr.setRequestHeader('Authorization', authHeader);
+            }
           });
 
         }
       };
     }).run(function(workspace, viewRegistry, helpRegistry, layoutFull) {
diff --git a/hawtio-system/src/main/java/io/hawt/web/LoginServlet.java b/hawtio-system/src/main/java/io/hawt/web/LoginServlet.java
index ab7345b4..0d9a012c 100644
--- a/hawtio-system/src/main/java/io/hawt/web/LoginServlet.java
+++ b/hawtio-system/src/main/java/io/hawt/web/LoginServlet.java
@@ -1,44 +1,39 @@
 package io.hawt.web;
 
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.security.AccessControlContext;
-import java.security.AccessController;
-import java.security.Principal;
-import java.util.ArrayList;
-import java.util.GregorianCalendar;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import io.hawt.system.ConfigManager;
+import io.hawt.system.Helpers;
+import org.jolokia.converter.Converters;
+import org.jolokia.converter.json.JsonConvertOptions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import javax.security.auth.Subject;
 import javax.servlet.ServletConfig;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpSession;
-
-import io.hawt.system.ConfigManager;
-import io.hawt.system.Helpers;
-import org.jolokia.converter.Converters;
-import org.jolokia.converter.json.JsonConvertOptions;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.security.AccessControlContext;
+import java.security.AccessController;
+import java.security.Principal;
+import java.util.*;
 
 /**
  *
  */
 public class LoginServlet extends HttpServlet {
 
     private static final long serialVersionUID = 1L;
     private static final transient Logger LOG = LoggerFactory.getLogger(LoginServlet.class);
 
-    Converters converters = new Converters();
-    JsonConvertOptions options = JsonConvertOptions.DEFAULT;
-    ConfigManager config;
+    protected Converters converters = new Converters();
+    protected JsonConvertOptions options = JsonConvertOptions.DEFAULT;
+    protected ConfigManager config;
     private Integer timeout;
 
     @Override
     public void init(ServletConfig servletConfig) throws ServletException {
         config = (ConfigManager) servletConfig.getServletContext().getAttribute("ConfigManager");
@@ -72,12 +67,13 @@ public class LoginServlet extends HttpServlet {
             Subject subject = (Subject) session.getAttribute("subject");
             if (subject == null) {
                 LOG.warn("No security subject stored in existing session, invalidating");
                 session.invalidate();
                 Helpers.doForbidden(resp);
+                return;
             }
-            returnPrincipals(subject, out);
+            sendResponse(session, subject, out);
             return;
         }
 
         AccessControlContext acc = AccessController.getContext();
         Subject subject = Subject.getSubject(acc);
@@ -110,14 +106,14 @@ public class LoginServlet extends HttpServlet {
         }
         if (LOG.isDebugEnabled()) {
             LOG.debug("Http session timeout for user {} is {} sec.", username, session.getMaxInactiveInterval());
         }
 
-        returnPrincipals(subject, out);
+        sendResponse(session, subject, out);
     }
 
-    private void returnPrincipals(Subject subject, PrintWriter out) {
+    protected void sendResponse(HttpSession session, Subject subject, PrintWriter out) {
 
         Map<String, Object> answer = new HashMap<String, Object>();
 
         List<Object> principals = new ArrayList<Object>();
 
diff --git a/hawtio-system/src/main/java/io/hawt/web/LoginTokenServlet.java b/hawtio-system/src/main/java/io/hawt/web/LoginTokenServlet.java
new file mode 100644
index 00000000..615dd923
--- /dev/null
+++ b/hawtio-system/src/main/java/io/hawt/web/LoginTokenServlet.java
@@ -0,0 +1,42 @@
+package io.hawt.web;
+
+import org.apache.commons.codec.binary.Base64;
+
+import javax.security.auth.Subject;
+import javax.servlet.http.HttpSession;
+import java.io.PrintWriter;
+import java.security.SecureRandom;
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * @author Stan Lewis
+ */
+public class LoginTokenServlet extends LoginServlet {
+
+    private static final long serialVersionUID = 1L;
+
+    public static final String LOGIN_TOKEN = "LoginToken";
+
+    @Override
+    protected void sendResponse(HttpSession session, Subject subject, PrintWriter out) {
+
+        String token = (String) session.getAttribute(LOGIN_TOKEN);
+
+        if ( token == null) {
+            byte[] seed = (subject.toString() + new Long(System.currentTimeMillis()).toString()).getBytes();
+            SecureRandom random = new SecureRandom(seed);
+            byte[] tokenBytes = new byte[128];
+            random.nextBytes(tokenBytes);
+            token = Base64.encodeBase64String(tokenBytes);
+            session.setAttribute(LOGIN_TOKEN, token);
+        }
+
+        Map<String, String> answer = new HashMap<String, String>();
+        answer.put("token", token);
+
+        ServletHelpers.writeObject(converters, options, out, answer);
+    }
+
+}

Vulnerable code: import javax.security.auth.Subject;
import java.security.AccessControlContext;
import java.security.AccessController;
        if (session == null) {
          AccessControlContext acc = AccessController.getContext();
          Subject subject = Subject.getSubject(acc);
          if (subject == null) {
          }
          session = request.getSession(true);
          session.setAttribute("subject", subject);
        } else {
          Subject subject = (Subject) session.getAttribute("subject");
          if (subject == null) {
            session.invalidate();
          }
gogo.Terminal_ctor = function(div, width, height, authHeader) {
           r.setRequestHeader('Authorization', authHeader);
gogo.Terminal = function(div, width, height, authHeader) {
   return new this.Terminal_ctor(div, width, height, authHeader);
          gogo.Terminal(element.get(0), width, height, authHeader);

          scope.$on("$destroy", function(e) {
            document.onkeypress = null;
            document.onkeydown = null;
import java.io.IOException;
import java.io.PrintWriter;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.Principal;
import java.util.ArrayList;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import io.hawt.system.ConfigManager;
import io.hawt.system.Helpers;
import org.jolokia.converter.Converters;
import org.jolokia.converter.json.JsonConvertOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
    Converters converters = new Converters();
    JsonConvertOptions options = JsonConvertOptions.DEFAULT;
    ConfigManager config;
            returnPrincipals(subject, out);
        returnPrincipals(subject, out);
    private void returnPrincipals(Subject subject, PrintWriter out) {
Fixed code: import io.hawt.web.LoginTokenServlet;
        String token = request.getHeader(LoginTokenServlet.LOGIN_TOKEN);

        if (token == null || session == null) {
        }

        String sessionToken = (String) session.getAttribute(LoginTokenServlet.LOGIN_TOKEN);
        if (!token.equals(sessionToken)) {
  <filter-mapping>
    <filter-name>AuthenticationFilter</filter-name>
    <url-pattern>/auth/*</url-pattern>
  </filter-mapping>

  <servlet>
    <servlet-name>login</servlet-name>
    <servlet-class>io.hawt.web.LoginTokenServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>login</servlet-name>
    <url-pattern>/auth/login/*</url-pattern>
  </servlet-mapping>
  <servlet>
    <servlet-name>logout</servlet-name>
    <servlet-class>io.hawt.web.LogoutServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>logout</servlet-name>
    <url-pattern>/auth/logout/*</url-pattern>
  </servlet-mapping>
gogo.Terminal_ctor = function(div, width, height, token) {
           r.setRequestHeader('LoginToken', token);
gogo.Terminal = function(div, width, height, token) {
   return new this.Terminal_ctor(div, width, height, token);
          scope.$on("$destroy", function(e) {
            scope.destroyed = true;
            document.onkeypress = null;
            document.onkeydown = null;
            if (!('term' in scope)) {
              return;
            }
            var url = "hawtio-karaf-terminal/auth/logout/";
            delete scope.term;
            $.ajax(url, {
              type: "POST",
              success: function (response) {
                log.debug("logged out of terminal");
                Core.$apply(scope);
              },
              error: function (xhr, textStatus, error) {
                log.info("Failed to log out of terminal: ", error);
              },
              beforeSend: function (xhr) {
                xhr.setRequestHeader('Authorization', authHeader);
              }
            })
          });

          if (scope.destroyed) {
            return;
          }

          var url = "hawtio-karaf-terminal/auth/login/";

          $.ajax(url, {
            type: "POST",
            success: function (response) {
              if (scope.destroyed) {
                log.debug("Scope's been destroyed since we made our request, let's not create a terminal instance");
                return;
              }
              log.debug("got back response: ", response);
              if ('term' in scope) {
                log.debug("Previous terminal created, let's clean it up");
                document.onkeypress = null;
                document.onkeydown = null;
                delete scope.term;
              }
              scope.term = gogo.Terminal(element.get(0), width, height, response['token']);
              Core.$apply(scope);

            },
            error: function (xhr, textStatus, error) {
              log.warn("Failed to log into terminal: ", error);
            },
            beforeSend: function (xhr) {
              xhr.setRequestHeader('Authorization', authHeader);
            }
import io.hawt.system.ConfigManager;
import io.hawt.system.Helpers;
import org.jolokia.converter.Converters;
import org.jolokia.converter.json.JsonConvertOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.PrintWriter;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.Principal;
import java.util.*;
    protected Converters converters = new Converters();
    protected JsonConvertOptions options = JsonConvertOptions.DEFAULT;
    protected ConfigManager config;
                return;
            sendResponse(session, subject, out);
        sendResponse(session, subject, out);
    protected void sendResponse(HttpSession session, Subject subject, PrintWriter out) {
package io.hawt.web;

import org.apache.commons.codec.binary.Base64;

import javax.security.auth.Subject;
import javax.servlet.http.HttpSession;
import java.io.PrintWriter;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;


/**
 * @author Stan Lewis
 */
public class LoginTokenServlet extends LoginServlet {

    private static final long serialVersionUID = 1L;

    public static final String LOGIN_TOKEN = "LoginToken";

    @Override
    protected void sendResponse(HttpSession session, Subject subject, PrintWriter out) {

        String token = (String) session.getAttribute(LOGIN_TOKEN);

        if ( token == null) {
            byte[] seed = (subject.toString() + new Long(System.currentTimeMillis()).toString()).getBytes();
            SecureRandom random = new SecureRandom(seed);
            byte[] tokenBytes = new byte[128];
            random.nextBytes(tokenBytes);
            token = Base64.encodeBase64String(tokenBytes);
            session.setAttribute(LOGIN_TOKEN, token);
        }

        Map<String, String> answer = new HashMap<String, String>();
        answer.put("token", token);

        ServletHelpers.writeObject(converters, options, out, answer);
    }

}