CVE-2015-8814 (published 2017-03-03T16:59Z)

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.

Severity: MEDIUM
CVSS v2 base score: 6.8 (AV:N/AC:M/Au:N/C:P/I:P/A:P)
CVSS v3 base score: 8.8, HIGH (CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-352
cwe_name: Cross-Site Request Forgery (CSRF)
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.
extended_description: When a web server is designed to receive a request from a client without any mechanism for verifying that it was intentionally sent, then it might be possible for an attacker to trick a client into making an unintentional request to the web server which will be treated as an authentic request. This can be done via a URL, image load, XMLHttpRequest, etc. and can result in exposure of data or unintended code execution.
         

Fix information:
repo_url: https://github.com/umbraco/Umbraco-CMS
commit_details: repo_url: https://github.com/umbraco/Umbraco-CMS
author: Shannon
author_date: 2015-11-25 19:39:24+01:00
msg: Fixes U4-7459 XSRF protection bypass  - ensures tokens are checked for the non-editor api controllers
repository: repo_url: https://github.com/umbraco/Umbraco-CMS
repo_name: umbraco/Umbraco-CMS
description: Umbraco is a free and open source .NET content management system helping you deliver delightful digital experiences.
repo_language: C#
owner: umbraco
file_changes: filename: Extensions.js
change_type: ModificationType.MODIFY
diff: @@ -357,4 +357,20 @@
 
     });
 
+    //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

programming_language: JavaScript
filename: ValidateMvcAngularAntiForgeryTokenAttribute.cs
change_type: ModificationType.ADD
diff: @@ -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

programming_language: C#
filename: Umbraco.Web.csproj
change_type: ModificationType.MODIFY
diff: @@ -306,6 +306,7 @@
     <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" />

programming_language: PowerShell
filename: AngularAntiForgeryHelper.cs
change_type: ModificationType.MODIFY
diff: @@ -1,4 +1,6 @@
 ﻿using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
 using System.Linq;
 using System.Net.Http;
 using System.Net.Http.Headers;
@@ -28,6 +30,8 @@ public static class AngularAntiForgeryHelper
         /// </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>
@@ -64,13 +68,10 @@ public static bool ValidateTokens(string cookieToken, string headerToken)
             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 = "";
 
@@ -85,12 +86,7 @@ public static bool ValidateHeaders(HttpRequestHeaders requestHeaders, out string
                 .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)
             {
@@ -98,13 +94,32 @@ public static bool ValidateHeaders(HttpRequestHeaders requestHeaders, out string
                 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

programming_language: C#
filename: BulkPublishController.cs
change_type: ModificationType.MODIFY
diff: @@ -15,6 +15,7 @@ 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>

programming_language: C#
filename: DomainsApiController.cs
change_type: ModificationType.MODIFY
diff: @@ -13,6 +13,7 @@
 //using umbraco.cms.businesslogic.language;
 using umbraco.BusinessLogic.Actions;
 using umbraco.cms.businesslogic.web;
+using Umbraco.Web.WebApi.Filters;
 
 namespace Umbraco.Web.WebServices
 {
@@ -20,6 +21,7 @@ namespace Umbraco.Web.WebServices
     /// A REST controller used for managing domains.
     /// </summary>
     /// <remarks>Nothing to do with Active Directory.</remarks>
+    [ValidateAngularAntiForgeryToken]
     public class DomainsApiController : UmbracoAuthorizedApiController
     {
         [HttpPost]

programming_language: C#
filename: ExamineManagementApiController.cs
change_type: ModificationType.MODIFY
diff: @@ -13,9 +13,11 @@
 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>

programming_language: C#
filename: SaveFileController.cs
change_type: ModificationType.MODIFY
diff: @@ -26,6 +26,7 @@ namespace Umbraco.Web.WebServices
     /// 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>

programming_language: C#
filename: XmlDataIntegrityController.cs
change_type: ModificationType.MODIFY
diff: @@ -4,10 +4,11 @@
 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]

programming_language: C#