CVE-2016-0721: Session Fixation (CWE-384)
value: Session fixation vulnerability in pcsd in pcs before 0.9.157.
CWE description: Authenticating a user, or otherwise establishing a new user session, without invalidating any existing session identifier gives an attacker the opportunity to steal authenticated sessions.
CVSS v3 base score: 8.1
Published: 2017-04-21T15:59Z
Repository: https://github.com/ClusterLabs/pcs
Language: Ruby
Files changed: pcsd/pcsd.rb, pcsd/public/js/nodes-ember.js, pcsd/public/js/pcsd.js, pcsd/views/_dialogs.erb, pcsd/views/permissions.erb
Commit date: 2016-02-15 15:57:36 +0100
Commit message: web UI auth: add js/ajax login dialog

Diff: diff --git a/pcsd/pcsd.rb b/pcsd/pcsd.rb
index 7a00a8ba..e5eb3d03 100644
--- a/pcsd/pcsd.rb
+++ b/pcsd/pcsd.rb
@@ -143,10 +143,14 @@ $thread_session_expired = Thread.new {
     end
   end
 }
 
 helpers do
+  def is_ajax?
+    return request.env['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'
+  end
+
   def protected!
     gui_request = ( # these are URLs for web pages
       request.path == '/' or
       request.path == '/manage' or
       request.path == '/permissions' or
@@ -156,13 +160,11 @@ helpers do
       @auth_user = PCSAuth.loginByToken(cookies)
       unless @auth_user
         halt [401, '{"notauthorized":"true"}']
       end
     else #/managec/* /manage/* /permissions
-      if !gui_request and
-        request.env['HTTP_X_REQUESTED_WITH'] != 'XMLHttpRequest'
-      then
+      if !gui_request and !is_ajax? then
         # Accept non GUI requests only with header
         # "X_REQUESTED_WITH: XMLHttpRequest". (check if they are send via AJAX).
         # This prevents CSRF attack.
         halt [401, '{"notauthorized":"true"}']
       elsif not PCSAuth.isLoggedIn(session)
@@ -359,13 +361,13 @@ post '/run_pcs' do
 end
 
 if not DISABLE_GUI
   get('/login'){ erb :login, :layout => :main }
 
-  get '/logout' do 
+  get '/logout' do
     session.destroy
-    erb :login, :layout => :main
+    redirect '/login'
   end
 
   post '/login' do
     auth_user = PCSAuth.loginByPassword(
       params['username'], params['password']
@@ -381,15 +383,23 @@ if not DISABLE_GUI
       #      if plp == "" or plp == "/"
       #        plp = '/manage'
       #      end
       #      redirect plp
       #    else
-      redirect '/manage'
+      if is_ajax?
+        halt [200, "OK"]
+      else
+        redirect '/manage'
+      end
       #    end
     else
-      session["bad_login_name"] = params['username']
-      redirect '/login?badlogin=1'
+      if is_ajax?
+        halt [401, '{"notauthorized":"true"}']
+      else
+        session["bad_login_name"] = params['username']
+        redirect '/login?badlogin=1'
+      end
     end
   end
 
   post '/manage/existingcluster' do
     pcs_config = PCSConfig.new(Cfgsync::PcsdSettings.from_file('{}').text())
diff --git a/pcsd/public/js/nodes-ember.js b/pcsd/public/js/nodes-ember.js
index d49feb8f..237d7cea 100644
--- a/pcsd/public/js/nodes-ember.js
+++ b/pcsd/public/js/nodes-ember.js
@@ -67,11 +67,11 @@ Pcs = Ember.Application.createWithMixins({
     if (cluster_name == null) {
       if (location.pathname.indexOf("/manage") != 0) {
         return;
       }
       Ember.debug("Empty Cluster Name");
-      $.ajax({
+      ajax_wrapper({
         url: "/clusters_overview",
         dataType: "json",
         timeout: 20000,
         success: function(data) {
           Pcs.clusterController.update(data);
@@ -100,11 +100,11 @@ Pcs = Ember.Application.createWithMixins({
           Pcs.get('updater').update_finished();
         }
       });
       return;
     }
-    $.ajax({
+    ajax_wrapper({
       url: "cluster_status",
       dataType: "json",
       success: function(data) {
         Pcs.resourcesContainer.update(data);
         Pcs.nodesController.update(data);
@@ -500,11 +500,11 @@ Pcs.resourcesContainer = Ember.Object.create({
       res_id: resource_id,
       key: attr,
       value: value
     };
 
-    $.ajax({
+    ajax_wrapper({
       type: 'POST',
       url: get_cluster_remote_url() + 'add_meta_attr_remote',
       data: data,
       timeout: pcs_timeout,
       error: function (xhr, status, error) {
@@ -521,11 +521,11 @@ Pcs.resourcesContainer = Ember.Object.create({
 
   enable_resource: function(resource_id) {
     if (resource_id == null) {
       return;
     }
-    $.ajax({
+    ajax_wrapper({
       type: 'POST',
       url: get_cluster_remote_url() + 'resource_start',
       data: {resource: resource_id},
       timeout: pcs_timeout,
       success: function(data) {
@@ -547,11 +547,11 @@ Pcs.resourcesContainer = Ember.Object.create({
 
   disable_resource: function(resource_id) {
     if (resource_id == null) {
       return;
     }
-    $.ajax({
+    ajax_wrapper({
       type: 'POST',
       url: get_cluster_remote_url() + 'resource_stop',
       data: {resource: resource_id},
       timeout: pcs_timeout,
       success: function(data) {
diff --git a/pcsd/public/js/pcsd.js b/pcsd/public/js/pcsd.js
index b7bc712e..25cc1ac0 100644
--- a/pcsd/public/js/pcsd.js
+++ b/pcsd/public/js/pcsd.js
@@ -1,6 +1,8 @@
 var pcs_timeout = 30000;
+var login_dialog_opened = false;
+var ajax_queue = Array();
 
 function curResource() {
   var obj = Pcs.resourcesContainer.get('cur_resource');
   if (obj == null) {
     return null;
@@ -118,11 +120,11 @@ function create_group() {
         $(this).dialog("close");
       },
       "Create Group": function() {
         var data = $('#add_group > form').serialize();
         var url = get_cluster_remote_url() + "add_group";
-        $.ajax({
+        ajax_wrapper({
           type: "POST",
           url: url,
           data: data,
           success: function() {
             Pcs.update();
@@ -185,11 +187,11 @@ function checkAddingNode(){
   if (nodeName == "") {
     $("#add_node_submit_btn").button("option", "disabled", false);
     return false;
   }
 
-  $.ajax({
+  ajax_wrapper({
     type: 'GET',
     url: '/manage/check_pcsd_status',
     data: {"nodes": nodeName},
     timeout: pcs_timeout,
     success: function (data) {
@@ -211,11 +213,11 @@ function checkAddingNode(){
   });
 }
 
 function create_node(form) {
   var dataString = $(form).serialize();
-  $.ajax({
+  ajax_wrapper({
     type: "POST",
     url: get_cluster_remote_url() + "add_node_to_cluster",
     data: dataString,
     success: function(returnValue) {
       $("#add_node_submit_btn").button("option", "disabled", false);
@@ -240,11 +242,11 @@ function create_resource(form, update, stonith) {
   if (stonith)
     name = "fence device";
   else
     name = "resource"
 
-  $.ajax({
+  ajax_wrapper({
     type: "POST",
     url: url,
     data: dataString,
     dataType: "json",
     success: function(returnValue) {
@@ -480,11 +482,11 @@ function fade_in_out(id) {
 }
 
 function node_link_action(link_selector, url, label) {
   var node = $.trim($("#node_info_header_title_name").text());
   fade_in_out(link_selector);
-  $.ajax({
+  ajax_wrapper({
     type: 'POST',
     url: url,
     data: {"name": node},
     success: function() {
     },
@@ -499,11 +501,11 @@ function node_link_action(link_selector, url, label) {
 
 function setup_node_links() {
   Ember.debug("Setup node links");
   $("#node_start").click(function() {
     node_link_action(
-      "#node_start", get_cluster_remote_url() +"cluster_start", "start"
+      "#node_start", get_cluster_remote_url() + "cluster_start", "start"
     );
   });
   $("#node_stop").click(function() {
     var node = $.trim($("#node_info_header_title_name").text());
     fade_in_out("#node_stop");
@@ -532,11 +534,11 @@ function node_stop(node, force) {
   var data = {};
   data["name"] = node;
   if (force) {
     data["force"] = force;
   }
-  $.ajax({
+  ajax_wrapper({
     type: 'POST',
     url: get_cluster_remote_url() + 'cluster_stop',
     data: data,
     timeout: pcs_timeout,
     success: function() {
@@ -550,11 +552,11 @@ function node_stop(node, force) {
          - if pcs is not able to stop a node it returns an (forceable) error
            immediatelly
         */
         return;
       }
-      var message = "Unable to stop node '" + node + " " + ajax_simple_error(
+      var message = "Unable to stop node '" + node + "' " + ajax_simple_error(
         xhr, status, error
       );
       if (message.indexOf('--force') == -1) {
         alert(message);
       }
@@ -582,11 +584,11 @@ function cleanup_resource() {
   var resource = curResource();
   if (resource == null) {
     return;
   }
   fade_in_out("#resource_cleanup_link");
-  $.ajax({
+  ajax_wrapper({
     type: 'POST',
     url: get_cluster_remote_url() + 'resource_cleanup',
     data: {"resource": resource},
     success: function() {
     },
@@ -603,11 +605,11 @@ function cleanup_stonith() {
   var resource = curStonith();
   if (resource == null) {
     return;
   }
   fade_in_out("#stonith_cleanup_link");
-  $.ajax({
+  ajax_wrapper({
     type: 'POST',
     url: get_cluster_remote_url() + 'resource_cleanup',
     data: {"resource": resource},
     success: function() {
     },
@@ -624,11 +626,11 @@ function checkExistingNode() {
   var node = "";
   $('input[name="node-name"]').each(function(i,e) {
     node = e.value;
   });
 
-  $.ajax({
+  ajax_wrapper({
     type: 'GET',
     url: '/manage/check_pcsd_status',
     data: {"nodes": node},
     timeout: pcs_timeout,
     success: function (data) {
@@ -648,18 +650,18 @@ function checkClusterNodes() {
     if (e.value != "") {
       nodes.push(e.value)
     }
   });
 
-  $.ajax({
+  ajax_wrapper({
     type: 'GET',
     url: '/manage/check_pcsd_status',
     data: {"nodes": nodes.join(",")},
     timeout: pcs_timeout,
     success: function (data) {
       mydata = jQuery.parseJSON(data);
-      $.ajax({
+      ajax_wrapper({
         type: 'GET',
         url: '/manage/get_nodes_sw_versions',
         data: {"nodes": nodes.join(",")},
         timeout: pcs_timeout,
         success: function(data) {
@@ -677,11 +679,11 @@ function checkClusterNodes() {
   });
 }
 
 function auth_nodes(dialog) {
   $("#auth_failed_error_msg").hide();
-  $.ajax({
+  ajax_wrapper({
     type: 'POST',
     url: '/manage/auth_gui_against_nodes',
     data: dialog.find("#auth_nodes_form").serialize(),
     timeout: pcs_timeout,
     success: function (data) {
@@ -850,11 +852,11 @@ function add_existing_dialog() {
 }
 
 function update_existing_cluster_dialog(data) {
   for (var i in data) {
     if (data[i] == "Online") {
-      $.ajax({
+      ajax_wrapper({
         type: "POST",
         url: "/manage/existingcluster",
         timeout: pcs_timeout,
         data: $('#add_existing_cluster_form').serialize(),
         success: function(data) {
@@ -1041,11 +1043,11 @@ function update_create_cluster_dialog(nodes, version_info) {
     $("#cman_mismatch_error_msg").hide();
     $("#rhel_version_mismatch_error_msg").hide();
   }
 
   if (good_nodes != 0 && cant_connect_nodes == 0 && cant_auth_nodes.length == 0 && cluster_name != "" && addr1_match == 1 && versions_check_ok == 1) {
-    $.ajax({
+    ajax_wrapper({
       type: "POST",
       url: "/manage/newcluster",
       timeout: pcs_timeout,
       data: $('#create_new_cluster_form').serialize(),
       success: function(data) {
@@ -1199,11 +1201,11 @@ function load_agent_form(resource_id, stonith) {
   if (!resource_obj || !resource_obj.get('is_primitive'))
     return;
 
   var data = {resource: resource_id};
 
-  $.ajax({
+  ajax_wrapper({
     type: 'GET',
     url: url,
     data: data,
     timeout: pcs_timeout,
     success: function (data) {
@@ -1239,11 +1241,11 @@ function destroy_tooltips() {
 function remove_cluster(ids) {
   var data = {};
   $.each(ids, function(_, cluster) {
     data[ "clusterid-" + cluster] = true;
   });
-  $.ajax({
+  ajax_wrapper({
     type: 'POST',
     url: '/manage/removecluster',
     data: data,
     timeout: pcs_timeout,
     success: function () {
@@ -1264,11 +1266,11 @@ function remove_nodes(ids, force) {
   }
   if (force) {
     data["force"] = force;
   }
 
-  $.ajax({
+  ajax_wrapper({
     type: 'POST',
     url: get_cluster_remote_url() + 'remove_nodes',
     data: data,
     timeout: pcs_timeout*3,
     success: function(data,textStatus) {
@@ -1319,11 +1321,11 @@ function remove_resource(ids, force) {
     } else if ($.inArray(res_obj.get("parent_id"), ids) == -1) {
       data["resid-" + id] = true;
     }
   });
 
-  $.ajax({
+  ajax_wrapper({
     type: 'POST',
     url: get_cluster_remote_url() + 'remove_resource',
     data: data,
     timeout: pcs_timeout*3,
     success: function () {
@@ -1360,11 +1362,11 @@ function add_remove_fence_level(parent_id,remove) {
     data["level"] = parent_id.parent().find("input[name='new_level_level']").val();
     data["devices"] = parent_id.parent().find("select[name='new_level_value']").val();
     data["node"] = Pcs.nodesController.cur_node.name;
   }
   fade_in_out(parent_id.parent());
-  $.ajax({
+  ajax_wrapper({
     type: 'POST',
     url: get_cluster_remote_url() + 'add_fence_level_remote',
     data: data,
     timeout: pcs_timeout,
     success: function() {
@@ -1401,11 +1403,11 @@ function remove_node_attr(parent_id) {
   data["node"] = Pcs.nodesController.cur_node.name;
   data["key"] = parent_id.attr("node_attr_key");
   data["value"] = ""; // empty value will remove attribute
   fade_in_out(parent_id.parent());
 
-  $.ajax({
+  ajax_wrapper({
     type: 'POST',
     url: get_cluster_remote_url() + 'add_node_attr_remote',
     data: data,
     timeout: pcs_timeout,
     success: function() {
@@ -1426,11 +1428,11 @@ function add_node_attr(parent_id) {
   data["node"] = Pcs.nodesController.cur_node.name;
   data["key"] = $(parent_id + " input[name='new_node_attr_key']").val();
   data["value"] = $(parent_id + " input[name='new_node_attr_value']").val();
   fade_in_out($(parent_id));
 
-  $.ajax({
+  ajax_wrapper({
     type: 'POST',
     url: get_cluster_remote_url() + 'add_node_attr_remote',
     data: data,
     timeout: pcs_timeout,
     success: function() {
@@ -1451,11 +1453,11 @@ function node_maintenance(node) {
   var data = {
     node: node,
     key: "maintenance",
     value: "on"
   };
-  $.ajax({
+  ajax_wrapper({
     type: 'POST',
     url: get_cluster_remote_url() + 'add_node_attr_remote',
     data: data,
     timeout: pcs_timeout,
     error: function (xhr, status, error) {
@@ -1474,11 +1476,11 @@ function node_unmaintenance(node) {
   var data = {
     node: node,
     key: "maintenance",
     value: ""
   };
-  $.ajax({
+  ajax_wrapper({
     type: 'POST',
     url: get_cluster_remote_url() + 'add_node_attr_remote',
     data: data,
     timeout: pcs_timeout,
     error: function (xhr, status, error) {
@@ -1531,11 +1533,11 @@ function add_constraint(parent_id, c_type, force) {
   if (force) {
     data["force"] = force;
   }
   fade_in_out($(parent_id));
 
-  $.ajax({
+  ajax_wrapper({
     type: 'POST',
     url: get_cluster_remote_url() + (
       data['node_id'] && (data['node_id'].trim().indexOf(' ') != -1)
       ? 'add_constraint_rule_remote'
       : 'add_constraint_remote'
@@ -1590,11 +1592,11 @@ function add_constraint_set(parent_id, c_type, force) {
   if (data['resources'].length < 1) {
     return;
   }
   fade_in_out($(parent_id))
 
-  $.ajax({
+  ajax_wrapper({
     type: "POST",
     url: get_cluster_remote_url() + "add_constraint_set_remote",
     data: data,
     timeout: pcs_timeout,
     success: function() {
@@ -1639,11 +1641,11 @@ function reset_constraint_set_form(parent_id) {
   );
 }
 
 function remove_constraint(id) {
   fade_in_out($("[constraint_id='"+id+"']").parent());
-  $.ajax({
+  ajax_wrapper({
     type: 'POST',
     url: get_cluster_remote_url() + 'remove_constraint_remote',
     data: {"constraint_id": id},
     timeout: pcs_timeout,
     error: function (xhr, status, error) {
@@ -1658,11 +1660,11 @@ function remove_constraint(id) {
   });
 }
 
 function remove_constraint_rule(id) {
   fade_in_out($("[rule_id='"+id+"']").parent());
-  $.ajax({
+  ajax_wrapper({
     type: 'POST',
     url: get_cluster_remote_url() + 'remove_constraint_rule_remote',
     data: {"rule_id": id},
     timeout: pcs_timeout,
     error: function (xhr, status, error) {
@@ -1679,11 +1681,11 @@ function remove_constraint_rule(id) {
 
 function add_acl_role(form) {
   var data = {}
   data["name"] = $(form).find("input[name='name']").val().trim();
   data["description"] = $(form).find("input[name='description']").val().trim();
-  $.ajax({
+  ajax_wrapper({
     type: "POST",
     url: get_cluster_remote_url() + "add_acl_role",
     data: data,
     success: function(data) {
       Pcs.update();
@@ -1702,11 +1704,11 @@ function add_acl_role(form) {
 function remove_acl_roles(ids) {
   var data = {};
   for (var i = 0; i < ids.length; i++) {
     data["role-" + i] = ids[i];
   }
-  $.ajax({
+  ajax_wrapper({
     type: "POST",
     url: get_cluster_remote_url() + "remove_acl_roles",
     data: data,
     timeout: pcs_timeout*3,
     success: function(data,textStatus) {
@@ -1745,11 +1747,11 @@ function add_acl_item(parent_id, item_type) {
       data["usergroup"] = $(parent_id + " input[name='role_assign_user']").val().trim();
       item_label = item_type
       break;
   }
   fade_in_out($(parent_id));
-  $.ajax({
+  ajax_wrapper({
     type: "POST",
     url: get_cluster_remote_url() + 'add_acl',
     data: data,
     timeout: pcs_timeout,
     success: function(data) {
@@ -1781,11 +1783,11 @@ function remove_acl_item(id,item) {
       data["role_id"] = id.attr("role_id")
       item_label = "user / group"
       break;
   }
 
-  $.ajax({
+  ajax_wrapper({
     type: 'POST',
     url: get_cluster_remote_url() + 'remove_acl',
     data: data,
     timeout: pcs_timeout,
     success: function (data) {
@@ -1807,11 +1809,11 @@ function update_cluster_settings() {
   };
   $.each(Pcs.settingsController.get("properties"), function(_, prop) {
     data[prop.get("form_name")] = prop.get("cur_val");
   });
   show_loading_screen();
-  $.ajax({
+  ajax_wrapper({
     type: 'POST',
     url: get_cluster_remote_url() + 'update_cluster_settings',
     data: data,
     timeout: pcs_timeout,
     success: function() {
@@ -1829,11 +1831,11 @@ function update_cluster_settings() {
 }
 
 function refresh_cluster_properties() {
   Pcs.settingsController.set("filter", "");
   $("#cluster_properties button").prop("disabled", true);
-  $.ajax({
+  ajax_wrapper({
     url: get_cluster_remote_url() + "cluster_properties",
     timeout: pcs_timeout,
     dataType: "json",
     success: function(data) {
       Pcs.settingsController.update(data);
@@ -2030,11 +2032,11 @@ function htmlEncode(s)
 }
 
 function fix_auth_of_cluster() {
   show_loading_screen();
   var clustername = Pcs.clusterController.cur_cluster.name;
-  $.ajax({
+  ajax_wrapper({
     url: get_cluster_remote_url(clustername) + "fix_auth_of_cluster",
     type: "POST",
     success: function(data) {
       hide_loading_screen();
       Pcs.update();
@@ -2127,11 +2129,11 @@ function tree_view_checkbox_onchange(element) {
 function resource_master(resource_id) {
   if (resource_id == null) {
     return;
   }
   show_loading_screen();
-  $.ajax({
+  ajax_wrapper({
     type: 'POST',
     url: get_cluster_remote_url() + 'resource_master',
     data: {resource_id: resource_id},
     timeout: pcs_timeout,
     error: function (xhr, status, error) {
@@ -2149,11 +2151,11 @@ function resource_master(resource_id) {
 function resource_clone(resource_id) {
   if (resource_id == null) {
     return;
   }
   show_loading_screen();
-  $.ajax({
+  ajax_wrapper({
     type: 'POST',
     url: get_cluster_remote_url() + 'resource_clone',
     data: {resource_id: resource_id},
     timeout: pcs_timeout,
     error: function (xhr, status, error) {
@@ -2175,11 +2177,11 @@ function resource_unclone(resource_id) {
   show_loading_screen();
   var resource_obj = Pcs.resourcesContainer.get_resource_by_id(resource_id);
   if (resource_obj.get('class_type') == 'clone') {
     resource_id = resource_obj.get('member').get('id');
   }
-  $.ajax({
+  ajax_wrapper({
     type: 'POST',
     url: get_cluster_remote_url() + 'resource_unclone',
     data: {resource_id: resource_id},
     timeout: pcs_timeout,
     error: function (xhr, status, error) {
@@ -2197,11 +2199,11 @@ function resource_unclone(resource_id) {
 function resource_ungroup(group_id) {
   if (group_id == null) {
     return;
   }
   show_loading_screen();
-  $.ajax({
+  ajax_wrapper({
     type: 'POST',
     url: get_cluster_remote_url() + 'resource_ungroup',
     data: {group_id: group_id},
     timeout: pcs_timeout,
     error: function (xhr, status, error) {
@@ -2234,11 +2236,11 @@ function resource_change_group(resource_id, group_id) {
     if (resource_obj.get('parent').get('class_type') == 'group') {
       data['old_group_id'] = resource_obj.get('parent').get('id');
     }
   }
 
-  $.ajax({
+  ajax_wrapper({
     type: 'POST',
     url: get_cluster_remote_url() + 'resource_change_group',
     data: data,
     timeout: pcs_timeout,
     error: function (xhr, status, error) {
@@ -2263,10 +2265,141 @@ function ajax_simple_error(xhr, status, error) {
     message = message + "\n\n" + $.trim(xhr.responseText);
   }
   return message;
 }
 
+function ajax_wrapper(options) {
+  // get original callback functions
+  var error_original = function(xhr, status, error) {};
+  if (options.error) {
+    error_original = options.error;
+  }
+  var complete_original = function(xhr, status) {};
+  if (options.complete) {
+    complete_original = options.complete;
+  }
+
+  // prepare new callback functions
+  var options_new = $.extend(true, {}, options);
+  // display login dialog on error
+  options_new.error = function(xhr, status, error) {
+    if (xhr.status == 401) {
+      ajax_queue.push(options);
+      if (!login_dialog_opened) {
+        login_dialog(function() {
+          var item;
+          while (ajax_queue.length > 0) {
+            item = ajax_queue.shift();
+            ajax_wrapper(item);
+          }
+        });
+      }
+    }
+    else {
+      error_original(xhr, status, error);
+    }
+  }
+  // Do not run complete function if login dialog is open.
+  // Once user is logged in again, the original complete function will be run
+  // in repeated ajax call run by login dialog on success.
+  options_new.complete = function(xhr, status) {
+    if (xhr.status == 401) {
+      return;
+    }
+    else {
+      complete_original(xhr, status);
+    }
+  }
+
+  // run ajax request or put it into a queue
+  if (login_dialog_opened) {
+    ajax_queue.push(options);
+  }
+  else {
+    $.ajax(options_new);
+  }
+}
+
+function login_dialog(on_success) {
+  var ok_button_id = "login_form_ok";
+  var ok_button_selector = "#" + ok_button_id;
+  var buttons = [
+    {
+      text: "Log In",
+      id: ok_button_id,
+      click: function() {
+        var me = $(this);
+        var my_dialog = $(this).dialog()
+        my_dialog.find("#login_form_denied").hide();
+        $(ok_button_selector).button("option", "disabled", true);
+        $.ajax({
+          type: "POST",
+          url: "/login",
+          data: my_dialog.find("#login_form").serialize(),
+          complete: function() {
+            $(ok_button_selector).button("option", "disabled", false);
+          },
+          success: function() {
+            my_dialog.find("#login_form_username").val("");
+            my_dialog.find("#login_form_password").val("");
+            me.dialog("destroy");
+            login_dialog_opened = false;
+            on_success();
+          },
+          error: function(xhr, status, error) {
+            if (xhr.status == 401) {
+              my_dialog.find("#login_form_denied").show();
+              my_dialog.find("#login_form_password").val("");
+            }
+            else {
+              alert("Login error " + ajax_simple_error(xhr, status, error));
+            }
+          },
+        });
+      },
+    },
+    {
+      text: "Cancel",
+      id: "login_form_cancel",
+      // cancel will close the dialog the same way as X button does
+      click: function() {
+        $(this).dialog("close");
+      },
+    },
+  ];
+  var dialog_obj = $("#dialog_login").dialog({
+    title: "Log In",
+    modal: true,
+    resizable: true,
+    width: 400,
+    buttons: buttons,
+    open: function(event, ui) {
+      login_dialog_opened = true;
+    },
+    create: function(event, ui) {
+      login_dialog_opened = true;
+    },
+    // make sure to logout the user on dialog close
+    close: function(event, ui) {
+      login_dialog_opened = false;
+      location = "/logout";
+    },
+  });
+  dialog_obj.find("#login_form_denied").hide();
+  // submit on enter
+  dialog_obj.keypress(function(e) {
+    if (
+      e.keyCode == $.ui.keyCode.ENTER
+      &&
+      !dialog_obj.parent().find(ok_button_selector).button("option", "disabled")
+    ) {
+      dialog_obj.parent().find(ok_button_selector).trigger("click");
+      return false;
+    }
+  });
+}
+
 var permissions_current_cluster;
 
 function permissions_load_all() {
   show_loading_screen();
 
@@ -2301,11 +2434,11 @@ function permissions_load_all() {
   }
 }
 
 function permissions_load_cluster(cluster_name, callback) {
   var element_id = "permissions_cluster_" + cluster_name;
-  $.ajax({
+  ajax_wrapper({
     type: "GET",
     url: "/permissions_cluster_form/" + cluster_name,
     timeout: pcs_timeout,
     success: function(data) {
       $("#" + element_id).html(data);
@@ -2347,11 +2480,11 @@ function permissions_show_cluster(cluster_name, list_row) {
 }
 
 function permissions_save_cluster(form) {
   var dataString = $(form).serialize();
   var cluster_name = permissions_get_clustername(form);
-  $.ajax({
+  ajax_wrapper({
     type: "POST",
     url: get_cluster_remote_url(cluster_name) + "permissions_save",
     timeout: pcs_timeout,
     data: dataString,
     success: function() {
@@ -2560,11 +2693,11 @@ function set_utilization(type, entity_id, name, value) {
   } else if (type == "resource") {
     data["resource_id"] = entity_id;
   } else return false;
   var url = get_cluster_remote_url() + "set_" + type + "_utilization";
 
-  $.ajax({
+  ajax_wrapper({
     type: 'POST',
     url: url,
     data: data,
     timeout: pcs_timeout,
     error: function (xhr, status, error) {
diff --git a/pcsd/views/_dialogs.erb b/pcsd/views/_dialogs.erb
index 40daaf2a..8bfa5c69 100644
--- a/pcsd/views/_dialogs.erb
+++ b/pcsd/views/_dialogs.erb
@@ -1,5 +1,24 @@
+<div id="dialog_login" style="display:none;">
+  <form id="login_form">
+    <div>Your session has expired. Log in again, please.</div>
+    <div id="login_form_denied" style="color:red; display:none;">
+      Bad username or password
+    </div>
+    <table>
+      <tr>
+        <td><label for="login_form_username">Username:</label></td>
+        <td><input type="text" id="login_form_username" name="username"></td>
+      </tr>
+      <tr>
+        <td><label for="login_form_password">Password:</label></td>
+        <td><input type="password" id="login_form_password" name="password"></td>
+      </tr>
+    </table>
+  </form>
+</div>
+
 <div id="auth_nodes" style="display:none;">
   <form id="auth_nodes_form">
     Enter password for user 'hacluster' to authenticate nodes.<br>
     Nodes to authenticate:
     <table class="err_msg_table" style="width: 100%">
diff --git a/pcsd/views/permissions.erb b/pcsd/views/permissions.erb
index 1e38d7eb..bfa5638f 100644
--- a/pcsd/views/permissions.erb
+++ b/pcsd/views/permissions.erb
@@ -50,5 +50,6 @@
         </div>
       <% } %>
     </td>
   </tr>
 </table>
+<%= erb :_dialogs %>

Vulnerable code:       if !gui_request and
        request.env['HTTP_X_REQUESTED_WITH'] != 'XMLHttpRequest'
      then
  get '/logout' do 
    erb :login, :layout => :main
      redirect '/manage'
      session["bad_login_name"] = params['username']
      redirect '/login?badlogin=1'
      $.ajax({
    $.ajax({
    $.ajax({
    $.ajax({
    $.ajax({
        $.ajax({
  $.ajax({
  $.ajax({
  $.ajax({
  $.ajax({
      "#node_start", get_cluster_remote_url() +"cluster_start", "start"
  $.ajax({
      var message = "Unable to stop node '" + node + " " + ajax_simple_error(
  $.ajax({
  $.ajax({
  $.ajax({
  $.ajax({
      $.ajax({
  $.ajax({
      $.ajax({
    $.ajax({
  $.ajax({
  $.ajax({
  $.ajax({
  $.ajax({
  $.ajax({
  $.ajax({
  $.ajax({
  $.ajax({
  $.ajax({
  $.ajax({
  $.ajax({
  $.ajax({
  $.ajax({
  $.ajax({
  $.ajax({
  $.ajax({
  $.ajax({
  $.ajax({
  $.ajax({
  $.ajax({
  $.ajax({
  $.ajax({
  $.ajax({
  $.ajax({
  $.ajax({
  $.ajax({
  $.ajax({
  $.ajax({
Fixed code:   def is_ajax?
    return request.env['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'
  end

      if !gui_request and !is_ajax? then
  get '/logout' do
    redirect '/login'
      if is_ajax?
        halt [200, "OK"]
      else
        redirect '/manage'
      end
      if is_ajax?
        halt [401, '{"notauthorized":"true"}']
      else
        session["bad_login_name"] = params['username']
        redirect '/login?badlogin=1'
      end
      ajax_wrapper({
    ajax_wrapper({
    ajax_wrapper({
    ajax_wrapper({
    ajax_wrapper({
var login_dialog_opened = false;
var ajax_queue = Array();
        ajax_wrapper({
  ajax_wrapper({
  ajax_wrapper({
  ajax_wrapper({
  ajax_wrapper({
      "#node_start", get_cluster_remote_url() + "cluster_start", "start"
  ajax_wrapper({
      var message = "Unable to stop node '" + node + "' " + ajax_simple_error(
  ajax_wrapper({
  ajax_wrapper({
  ajax_wrapper({
  ajax_wrapper({
      ajax_wrapper({
  ajax_wrapper({
      ajax_wrapper({
    ajax_wrapper({
  ajax_wrapper({
  ajax_wrapper({
  ajax_wrapper({
  ajax_wrapper({
  ajax_wrapper({
  ajax_wrapper({
  ajax_wrapper({
  ajax_wrapper({
  ajax_wrapper({
  ajax_wrapper({
  ajax_wrapper({
  ajax_wrapper({
  ajax_wrapper({
  ajax_wrapper({
  ajax_wrapper({
  ajax_wrapper({
  ajax_wrapper({
  ajax_wrapper({
  ajax_wrapper({
  ajax_wrapper({
  ajax_wrapper({
  ajax_wrapper({
  ajax_wrapper({
  ajax_wrapper({
  ajax_wrapper({
function ajax_wrapper(options) {
  // get original callback functions
  var error_original = function(xhr, status, error) {};
  if (options.error) {
    error_original = options.error;
  }
  var complete_original = function(xhr, status) {};
  if (options.complete) {
    complete_original = options.complete;
  }

  // prepare new callback functions
  var options_new = $.extend(true, {}, options);
  // display login dialog on error
  options_new.error = function(xhr, status, error) {
    if (xhr.status == 401) {
      ajax_queue.push(options);
      if (!login_dialog_opened) {
        login_dialog(function() {
          var item;
          while (ajax_queue.length > 0) {
            item = ajax_queue.shift();
            ajax_wrapper(item);
          }
        });
      }
    }
    else {
      error_original(xhr, status, error);
    }
  }
  // Do not run complete function if login dialog is open.
  // Once user is logged in again, the original complete function will be run
  // in repeated ajax call run by login dialog on success.
  options_new.complete = function(xhr, status) {
    if (xhr.status == 401) {
      return;
    }
    else {
      complete_original(xhr, status);
    }
  }

  // run ajax request or put it into a queue
  if (login_dialog_opened) {
    ajax_queue.push(options);
  }
  else {
    $.ajax(options_new);
  }
}

function login_dialog(on_success) {
  var ok_button_id = "login_form_ok";
  var ok_button_selector = "#" + ok_button_id;
  var buttons = [
    {
      text: "Log In",
      id: ok_button_id,
      click: function() {
        var me = $(this);
        var my_dialog = $(this).dialog()
        my_dialog.find("#login_form_denied").hide();
        $(ok_button_selector).button("option", "disabled", true);
        $.ajax({
          type: "POST",
          url: "/login",
          data: my_dialog.find("#login_form").serialize(),
          complete: function() {
            $(ok_button_selector).button("option", "disabled", false);
          },
          success: function() {
            my_dialog.find("#login_form_username").val("");
            my_dialog.find("#login_form_password").val("");
            me.dialog("destroy");
            login_dialog_opened = false;
            on_success();
          },
          error: function(xhr, status, error) {
            if (xhr.status == 401) {
              my_dialog.find("#login_form_denied").show();
              my_dialog.find("#login_form_password").val("");
            }
            else {
              alert("Login error " + ajax_simple_error(xhr, status, error));
            }
          },
        });
      },
    },
    {
      text: "Cancel",
      id: "login_form_cancel",
      // cancel will close the dialog the same way as X button does
      click: function() {
        $(this).dialog("close");
      },
    },
  ];
  var dialog_obj = $("#dialog_login").dialog({
    title: "Log In",
    modal: true,
    resizable: true,
    width: 400,
    buttons: buttons,
    open: function(event, ui) {
      login_dialog_opened = true;
    },
    create: function(event, ui) {
      login_dialog_opened = true;
    },
    // make sure to logout the user on dialog close
    close: function(event, ui) {
      login_dialog_opened = false;
      location = "/logout";
    },
  });
  dialog_obj.find("#login_form_denied").hide();
  // submit on enter
  dialog_obj.keypress(function(e) {
    if (
      e.keyCode == $.ui.keyCode.ENTER
      &&
      !dialog_obj.parent().find(ok_button_selector).button("option", "disabled")
    ) {
      dialog_obj.parent().find(ok_button_selector).trigger("click");
      return false;
    }
  });
}

  ajax_wrapper({
  ajax_wrapper({
  ajax_wrapper({
<div id="dialog_login" style="display:none;">
  <form id="login_form">
    <div>Your session has expired. Log in again, please.</div>
    <div id="login_form_denied" style="color:red; display:none;">
      Bad username or password
    </div>
    <table>
      <tr>
        <td><label for="login_form_username">Username:</label></td>
        <td><input type="text" id="login_form_username" name="username"></td>
      </tr>
      <tr>
        <td><label for="login_form_password">Password:</label></td>
        <td><input type="password" id="login_form_password" name="password"></td>
      </tr>
    </table>
  </form>
</div>

<%= erb :_dialogs %>