CVE-2016-0720: Cross-Site Request Forgery (CSRF) (CWE-352)
value: Cross-site request forgery (CSRF) vulnerability in pcsd web UI in pcs before 0.9.149.
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-04-21T15:59Z
Repository: https://github.com/ClusterLabs/pcs
Language: Ruby
Files changed: pcsd/pcsd.rb
Commit date: 2016-02-15 15:57:36 +0100
Commit message: fix CSRF vulnerability

 * requests /remote/* (GET and POST) and /run_pcs are accessible only with token authentication
 * all web UI requests are accessible only with session authentication
 * all web UI requests (except for /manage, /managec/<cluster>/main, /permissions ) must
   include header: X-Requested-With: XMLHttpRequest

Diff: diff --git a/pcsd/pcsd.rb b/pcsd/pcsd.rb
index 349e236..847eb11 100644
--- a/pcsd/pcsd.rb
+++ b/pcsd/pcsd.rb
@@ -119,30 +119,35 @@ $thread_cfgsync = Thread.new {
   end
 }
 
 helpers do
   def protected!
-    if not PCSAuth.loginByToken(session, cookies) and not PCSAuth.isLoggedIn(session)
-      # If we're on /managec/<cluster_name>/main we redirect
-      match_expr = "/managec/(.*)/(.*)"
-      mymatch = request.path.match(match_expr)
-      on_managec_main = false
-      if mymatch and mymatch.length >= 3 and mymatch[2] == "main"
-        on_managec_main = true
+    gui_request = ( # these are URLs for web pages
+      request.path == '/' or
+      request.path == '/manage' or
+      request.path == '/permissions' or
+      request.path.match('/managec/.+/main')
+    )
+    if request.path.start_with?('/remote/') or request.path == '/run_pcs'
+      unless PCSAuth.loginByToken(session, cookies)
+        halt [401, '{"notauthorized":"true"}']
       end
-
-      if request.path.start_with?('/remote') or
-        (request.path.match(match_expr) and not on_managec_main) or
-        '/run_pcs' == request.path or
-        '/clusters_overview' == request.path or
-        request.path.start_with?('/permissions_')
+    else #/managec/* /manage/* /permissions
+      if !gui_request and
+        request.env['HTTP_X_REQUESTED_WITH'] != 'XMLHttpRequest'
       then
-        $logger.info "ERROR: Request without authentication"
+        # 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"}']
-      else
-        session[:pre_login_path] = request.path
-        redirect '/login'
+      elsif not PCSAuth.isLoggedIn(session)
+        if gui_request
+          session[:pre_login_path] = request.path
+          redirect '/login'
+        else
+          halt [401, '{"notauthorized":"true"}']
+        end
       end
     end
   end
 
   def getParamList(params)

Vulnerable code:     if not PCSAuth.loginByToken(session, cookies) and not PCSAuth.isLoggedIn(session)
      # If we're on /managec/<cluster_name>/main we redirect
      match_expr = "/managec/(.*)/(.*)"
      mymatch = request.path.match(match_expr)
      on_managec_main = false
      if mymatch and mymatch.length >= 3 and mymatch[2] == "main"
        on_managec_main = true

      if request.path.start_with?('/remote') or
        (request.path.match(match_expr) and not on_managec_main) or
        '/run_pcs' == request.path or
        '/clusters_overview' == request.path or
        request.path.start_with?('/permissions_')
        $logger.info "ERROR: Request without authentication"
      else
        session[:pre_login_path] = request.path
        redirect '/login'
Fixed code:     gui_request = ( # these are URLs for web pages
      request.path == '/' or
      request.path == '/manage' or
      request.path == '/permissions' or
      request.path.match('/managec/.+/main')
    )
    if request.path.start_with?('/remote/') or request.path == '/run_pcs'
      unless PCSAuth.loginByToken(session, cookies)
        halt [401, '{"notauthorized":"true"}']
    else #/managec/* /manage/* /permissions
      if !gui_request and
        request.env['HTTP_X_REQUESTED_WITH'] != 'XMLHttpRequest'
        # Accept non GUI requests only with header
        # "X_REQUESTED_WITH: XMLHttpRequest". (check if they are send via AJAX).
        # This prevents CSRF attack.
      elsif not PCSAuth.isLoggedIn(session)
        if gui_request
          session[:pre_login_path] = request.path
          redirect '/login'
        else
          halt [401, '{"notauthorized":"true"}']
        end