CVE-2015-8814: Cross-Site Request Forgery (CSRF) (CWE-352)
value: Umbraco before 7.4.0 allows remote attackers to bypass anti-forgery security measures and conduct cross-site request forgery (CSRF) attacks as demonstrated by editing user account information in the templates.asmx.cs file.
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-03-03T16:59Z
Repository: https://github.com/umbraco/Umbraco-CMS
Language: C#
Files changed: src/Umbraco.Web.UI/umbraco_client/Application/Extensions.js, src/Umbraco.Web/Mvc/ValidateMvcAngularAntiForgeryTokenAttribute.cs, src/Umbraco.Web/Umbraco.Web.csproj, src/Umbraco.Web/WebApi/Filters/AngularAntiForgeryHelper.cs, src/Umbraco.Web/WebServices/BulkPublishController.cs, src/Umbraco.Web/WebServices/DomainsApiController.cs, src/Umbraco.Web/WebServices/ExamineManagementApiController.cs, src/Umbraco.Web/WebServices/SaveFileController.cs, src/Umbraco.Web/WebServices/XmlDataIntegrityController.cs
Commit date: 2015-11-25 19:39:24 +0100
Commit message: Fixes U4-7459 XSRF protection bypass  - ensures tokens are checked for the non-editor api controllers

Diff: diff --git a/src/Umbraco.Web.UI/umbraco_client/Application/Extensions.js b/src/Umbraco.Web.UI/umbraco_client/Application/Extensions.js
index 66c97c4db7..63870fad08 100644
--- a/src/Umbraco.Web.UI/umbraco_client/Application/Extensions.js
+++ b/src/Umbraco.Web.UI/umbraco_client/Application/Extensions.js
@@ -355,6 +355,22 @@
             };
         }
 
     });
 
+    //This sets the default jquery ajax headers to include our csrf token, we
+    // need to user the beforeSend method because our token changes per user/login so
+    // it cannot be static
+    $.ajaxSetup({
+        beforeSend: function (xhr) {
+
+            function getCookie(name) {
+                var value = "; " + document.cookie;
+                var parts = value.split("; " + name + "=");
+                if (parts.length === 2) return parts.pop().split(";").shift();
+            }
+
+            xhr.setRequestHeader("X-XSRF-TOKEN", getCookie("XSRF-TOKEN"));
+        }
+    });
+
 })(jQuery);
\ No newline at end of file
diff --git a/src/Umbraco.Web/Mvc/ValidateMvcAngularAntiForgeryTokenAttribute.cs b/src/Umbraco.Web/Mvc/ValidateMvcAngularAntiForgeryTokenAttribute.cs
new file mode 100644
index 0000000000..977f37473b
--- /dev/null
+++ b/src/Umbraco.Web/Mvc/ValidateMvcAngularAntiForgeryTokenAttribute.cs
@@ -0,0 +1,61 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Security.Claims;
+using System.Web.Mvc;
+using Umbraco.Web.WebApi.Filters;
+
+namespace Umbraco.Web.Mvc
+{
+    /// <summary>
+    /// A filter to check for the csrf token based on Angular's standard approach
+    /// </summary>
+    /// <remarks>
+    /// Code derived from http://ericpanorel.net/2013/07/28/spa-authentication-and-csrf-mvc4-antiforgery-implementation/
+    /// 
+    /// If the authentication type is cookie based, then this filter will execute, otherwise it will be disabled
+    /// </remarks>
+    public sealed class ValidateMvcAngularAntiForgeryTokenAttribute : ActionFilterAttribute
+    {
+        public override void OnActionExecuting(ActionExecutingContext filterContext)
+        {
+            var userIdentity = filterContext.HttpContext.User.Identity as ClaimsIdentity;
+            if (userIdentity != null)
+            {
+                //if there is not CookiePath claim, then exist
+                if (userIdentity.HasClaim(x => x.Type == ClaimTypes.CookiePath) == false)
+                {
+                    base.OnActionExecuting(filterContext);
+                    return;
+                }
+            }
+
+            string failedReason;
+            var headers = new List<KeyValuePair<string, List<string>>>();
+            foreach (var key in filterContext.HttpContext.Request.Headers.AllKeys)
+            {
+                if (headers.Any(x => x.Key == key))
+                {
+                    var found = headers.First(x => x.Key == key);
+                    found.Value.Add(filterContext.HttpContext.Request.Headers[key]);
+                }
+                else
+                {
+                    headers.Add(new KeyValuePair<string, List<string>>(key, new List<string> { filterContext.HttpContext.Request.Headers[key] }));
+                }
+            }
+            var cookie = filterContext.HttpContext.Request.Cookies[AngularAntiForgeryHelper.CsrfValidationCookieName];
+            if (AngularAntiForgeryHelper.ValidateHeaders(
+                headers.Select(x => new KeyValuePair<string, IEnumerable<string>>(x.Key, x.Value)).ToArray(),
+                cookie == null ? "" : cookie.Value,
+                out failedReason) == false)
+            {
+                var result = new HttpStatusCodeResult(HttpStatusCode.ExpectationFailed);
+                filterContext.Result = result;
+                return;
+            }
+
+            base.OnActionExecuting(filterContext);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj
index 1edf654af6..f84a2872ac 100644
--- a/src/Umbraco.Web/Umbraco.Web.csproj
+++ b/src/Umbraco.Web/Umbraco.Web.csproj
@@ -304,10 +304,11 @@
     <Compile Include="Media\EmbedProviders\Flickr.cs" />
     <Compile Include="Models\ContentEditing\SimpleNotificationModel.cs" />
     <Compile Include="Models\PublishedContentWithKeyBase.cs" />
     <Compile Include="Mvc\IRenderController.cs" />
     <Compile Include="Mvc\RenderIndexActionSelectorAttribute.cs" />
+    <Compile Include="Mvc\ValidateMvcAngularAntiForgeryTokenAttribute.cs" />
     <Compile Include="PropertyEditors\DatePreValueEditor.cs" />
     <Compile Include="RequestLifespanMessagesFactory.cs" />
     <Compile Include="Scheduling\LatchedBackgroundTaskBase.cs" />
     <Compile Include="Security\Identity\ExternalSignInAutoLinkOptions.cs" />
     <Compile Include="Security\Identity\FixWindowsAuthMiddlware.cs" />
diff --git a/src/Umbraco.Web/WebApi/Filters/AngularAntiForgeryHelper.cs b/src/Umbraco.Web/WebApi/Filters/AngularAntiForgeryHelper.cs
index d48cd66077..962183f7ef 100644
--- a/src/Umbraco.Web/WebApi/Filters/AngularAntiForgeryHelper.cs
+++ b/src/Umbraco.Web/WebApi/Filters/AngularAntiForgeryHelper.cs
@@ -1,6 +1,8 @@
 ﻿using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
 using System.Linq;
 using System.Net.Http;
 using System.Net.Http.Headers;
 using System.Web.Helpers;
 using Umbraco.Core;
@@ -26,10 +28,12 @@ public static class AngularAntiForgeryHelper
         /// <summary>
         /// The header name that angular uses to pass in the token to validate the cookie
         /// </summary>
         public const string AngularHeadername = "X-XSRF-TOKEN";
 
+        
+
         /// <summary>
         /// Returns 2 tokens - one for the cookie value and one that angular should set as the header value
         /// </summary>
         /// <param name="cookieToken"></param>
         /// <param name="headerToken"></param>
@@ -62,17 +66,14 @@ public static bool ValidateTokens(string cookieToken, string headerToken)
                 return false;
             }
             return true;
         }
 
-        /// <summary>
-        /// Validates the headers/cookies passed in for the request
-        /// </summary>
-        /// <param name="requestHeaders"></param>
-        /// <param name="failedReason"></param>
-        /// <returns></returns>
-        public static bool ValidateHeaders(HttpRequestHeaders requestHeaders, out string failedReason)
+        internal static bool ValidateHeaders(            
+            KeyValuePair<string, IEnumerable<string>>[] requestHeaders, 
+            string cookieToken,
+            out string failedReason)
         {
             failedReason = "";
 
             if (requestHeaders.Any(z => z.Key.InvariantEquals(AngularHeadername)) == false)
             {
@@ -83,28 +84,42 @@ public static bool ValidateHeaders(HttpRequestHeaders requestHeaders, out string
             var headerToken = requestHeaders
                 .Where(z => z.Key.InvariantEquals(AngularHeadername))
                 .Select(z => z.Value)
                 .SelectMany(z => z)
                 .FirstOrDefault();
-
-            var cookieToken = requestHeaders
-                .GetCookies()
-                .Select(c => c[CsrfValidationCookieName])
-                .FirstOrDefault();
-
+            
             // both header and cookie must be there
             if (cookieToken == null || headerToken == null)
             {
                 failedReason = "Missing token null";
                 return false;
             }
 
-            if (ValidateTokens(cookieToken.Value, headerToken) == false)
+            if (ValidateTokens(cookieToken, headerToken) == false)
             {
                 failedReason = "Invalid token";
                 return false;
             }
-            
+
             return true;
         }
+
+        /// <summary>
+        /// Validates the headers/cookies passed in for the request
+        /// </summary>
+        /// <param name="requestHeaders"></param>
+        /// <param name="failedReason"></param>
+        /// <returns></returns>
+        public static bool ValidateHeaders(HttpRequestHeaders requestHeaders, out string failedReason)
+        {
+            var cookieToken = requestHeaders
+                .GetCookies()
+                .Select(c => c[CsrfValidationCookieName])
+                .FirstOrDefault();
+
+            return ValidateHeaders(
+                requestHeaders.ToDictionary(x => x.Key, x => x.Value).ToArray(),
+                cookieToken == null ? null : cookieToken.Value,
+                out failedReason);
+        }
     }
 }
\ No newline at end of file
diff --git a/src/Umbraco.Web/WebServices/BulkPublishController.cs b/src/Umbraco.Web/WebServices/BulkPublishController.cs
index 30a609f6ff..cc9a153e07 100644
--- a/src/Umbraco.Web/WebServices/BulkPublishController.cs
+++ b/src/Umbraco.Web/WebServices/BulkPublishController.cs
@@ -13,10 +13,11 @@
 namespace Umbraco.Web.WebServices
 {
     /// <summary>
     /// A REST controller used for the publish dialog in order to publish bulk items at once
     /// </summary>
+    [ValidateMvcAngularAntiForgeryToken]
     public class BulkPublishController : UmbracoAuthorizedController
     {
         /// <summary>
         /// Publishes an document
         /// </summary>
diff --git a/src/Umbraco.Web/WebServices/DomainsApiController.cs b/src/Umbraco.Web/WebServices/DomainsApiController.cs
index 4f3509845d..8c146f2670 100644
--- a/src/Umbraco.Web/WebServices/DomainsApiController.cs
+++ b/src/Umbraco.Web/WebServices/DomainsApiController.cs
@@ -11,17 +11,19 @@
 using Umbraco.Web.Routing;
 using Umbraco.Web.WebApi;
 //using umbraco.cms.businesslogic.language;
 using umbraco.BusinessLogic.Actions;
 using umbraco.cms.businesslogic.web;
+using Umbraco.Web.WebApi.Filters;
 
 namespace Umbraco.Web.WebServices
 {
     /// <summary>
     /// A REST controller used for managing domains.
     /// </summary>
     /// <remarks>Nothing to do with Active Directory.</remarks>
+    [ValidateAngularAntiForgeryToken]
     public class DomainsApiController : UmbracoAuthorizedApiController
     {
         [HttpPost]
         // can't pass multiple complex args in json post request...
         public PostBackModel SaveLanguageAndDomains(PostBackModel model)
diff --git a/src/Umbraco.Web/WebServices/ExamineManagementApiController.cs b/src/Umbraco.Web/WebServices/ExamineManagementApiController.cs
index 01e9f30254..31ef4d23f2 100644
--- a/src/Umbraco.Web/WebServices/ExamineManagementApiController.cs
+++ b/src/Umbraco.Web/WebServices/ExamineManagementApiController.cs
@@ -11,13 +11,15 @@
 using Lucene.Net.Search;
 using Umbraco.Core;
 using Umbraco.Core.Logging;
 using Umbraco.Web.Search;
 using Umbraco.Web.WebApi;
+using Umbraco.Web.WebApi.Filters;
 
 namespace Umbraco.Web.WebServices
 {
+    [ValidateAngularAntiForgeryToken]
     public class ExamineManagementApiController : UmbracoAuthorizedApiController
     {
         /// <summary>
         /// Checks if the member internal index is consistent with the data stored in the database
         /// </summary>
diff --git a/src/Umbraco.Web/WebServices/SaveFileController.cs b/src/Umbraco.Web/WebServices/SaveFileController.cs
index f18f690b87..fb2c90a8c3 100644
--- a/src/Umbraco.Web/WebServices/SaveFileController.cs
+++ b/src/Umbraco.Web/WebServices/SaveFileController.cs
@@ -24,10 +24,11 @@ namespace Umbraco.Web.WebServices
     /// </summary>
     /// <remarks>
     /// This isn't fully implemented yet but we should migrate all of the logic in the umbraco.presentation.webservices.codeEditorSave
     /// over to this controller.
     /// </remarks>
+    [ValidateMvcAngularAntiForgeryToken]
     public class SaveFileController : UmbracoAuthorizedController
     {
         /// <summary>
         /// Saves a partial view macro
         /// </summary>
diff --git a/src/Umbraco.Web/WebServices/XmlDataIntegrityController.cs b/src/Umbraco.Web/WebServices/XmlDataIntegrityController.cs
index 6c1859dbfa..66951d12f2 100644
--- a/src/Umbraco.Web/WebServices/XmlDataIntegrityController.cs
+++ b/src/Umbraco.Web/WebServices/XmlDataIntegrityController.cs
@@ -2,14 +2,15 @@
 using System.Web.Http;
 using Umbraco.Core;
 using Umbraco.Core.Models.Rdbms;
 using Umbraco.Core.Persistence;
 using Umbraco.Web.WebApi;
+using Umbraco.Web.WebApi.Filters;
 
 namespace Umbraco.Web.WebServices
 {
-
+    [ValidateAngularAntiForgeryToken]
     public class XmlDataIntegrityController : UmbracoAuthorizedApiController
     {
         [HttpPost]
         public bool FixContentXmlTable()
         {

Vulnerable code:         /// <summary>
        /// Validates the headers/cookies passed in for the request
        /// </summary>
        /// <param name="requestHeaders"></param>
        /// <param name="failedReason"></param>
        /// <returns></returns>
        public static bool ValidateHeaders(HttpRequestHeaders requestHeaders, out string failedReason)

            var cookieToken = requestHeaders
                .GetCookies()
                .Select(c => c[CsrfValidationCookieName])
                .FirstOrDefault();

            if (ValidateTokens(cookieToken.Value, headerToken) == false)
            

Fixed code:     //This sets the default jquery ajax headers to include our csrf token, we
    // need to user the beforeSend method because our token changes per user/login so
    // it cannot be static
    $.ajaxSetup({
        beforeSend: function (xhr) {

            function getCookie(name) {
                var value = "; " + document.cookie;
                var parts = value.split("; " + name + "=");
                if (parts.length === 2) return parts.pop().split(";").shift();
            }

            xhr.setRequestHeader("X-XSRF-TOKEN", getCookie("XSRF-TOKEN"));
        }
    });

using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Security.Claims;
using System.Web.Mvc;
using Umbraco.Web.WebApi.Filters;

namespace Umbraco.Web.Mvc
{
    /// <summary>
    /// A filter to check for the csrf token based on Angular's standard approach
    /// </summary>
    /// <remarks>
    /// Code derived from http://ericpanorel.net/2013/07/28/spa-authentication-and-csrf-mvc4-antiforgery-implementation/
    /// 
    /// If the authentication type is cookie based, then this filter will execute, otherwise it will be disabled
    /// </remarks>
    public sealed class ValidateMvcAngularAntiForgeryTokenAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var userIdentity = filterContext.HttpContext.User.Identity as ClaimsIdentity;
            if (userIdentity != null)
            {
                //if there is not CookiePath claim, then exist
                if (userIdentity.HasClaim(x => x.Type == ClaimTypes.CookiePath) == false)
                {
                    base.OnActionExecuting(filterContext);
                    return;
                }
            }

            string failedReason;
            var headers = new List<KeyValuePair<string, List<string>>>();
            foreach (var key in filterContext.HttpContext.Request.Headers.AllKeys)
            {
                if (headers.Any(x => x.Key == key))
                {
                    var found = headers.First(x => x.Key == key);
                    found.Value.Add(filterContext.HttpContext.Request.Headers[key]);
                }
                else
                {
                    headers.Add(new KeyValuePair<string, List<string>>(key, new List<string> { filterContext.HttpContext.Request.Headers[key] }));
                }
            }
            var cookie = filterContext.HttpContext.Request.Cookies[AngularAntiForgeryHelper.CsrfValidationCookieName];
            if (AngularAntiForgeryHelper.ValidateHeaders(
                headers.Select(x => new KeyValuePair<string, IEnumerable<string>>(x.Key, x.Value)).ToArray(),
                cookie == null ? "" : cookie.Value,
                out failedReason) == false)
            {
                var result = new HttpStatusCodeResult(HttpStatusCode.ExpectationFailed);
                filterContext.Result = result;
                return;
            }

            base.OnActionExecuting(filterContext);
        }
    }
}
    <Compile Include="Mvc\ValidateMvcAngularAntiForgeryTokenAttribute.cs" />
using System.Collections.Generic;
using System.Collections.Specialized;
        

        internal static bool ValidateHeaders(            
            KeyValuePair<string, IEnumerable<string>>[] requestHeaders, 
            string cookieToken,
            out string failedReason)
            
            if (ValidateTokens(cookieToken, headerToken) == false)


        /// <summary>
        /// Validates the headers/cookies passed in for the request
        /// </summary>
        /// <param name="requestHeaders"></param>
        /// <param name="failedReason"></param>
        /// <returns></returns>
        public static bool ValidateHeaders(HttpRequestHeaders requestHeaders, out string failedReason)
        {
            var cookieToken = requestHeaders
                .GetCookies()
                .Select(c => c[CsrfValidationCookieName])
                .FirstOrDefault();

            return ValidateHeaders(
                requestHeaders.ToDictionary(x => x.Key, x => x.Value).ToArray(),
                cookieToken == null ? null : cookieToken.Value,
                out failedReason);
        }
    [ValidateMvcAngularAntiForgeryToken]
using Umbraco.Web.WebApi.Filters;
    [ValidateAngularAntiForgeryToken]
using Umbraco.Web.WebApi.Filters;
    [ValidateAngularAntiForgeryToken]
    [ValidateMvcAngularAntiForgeryToken]
using Umbraco.Web.WebApi.Filters;
    [ValidateAngularAntiForgeryToken]