CVE-2024-34071: Insufficient Information (NVD-CWE-noinfo)
value: Umbraco is an ASP.NET CMS used by more than 730.000 websites. Umbraco has an endpoint that is vulnerable to open redirects. The endpoint is protected so it requires the user to be signed into backoffice before the vulnerable is exposed. This vulnerability has been patched in version(s) 8.18.14, 10.8.6, 12.3.10 and 13.3.1.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-05-21T14:15Z
Repository: https://github.com/umbraco/Umbraco-CMS
Language: C#
Files changed: src/Umbraco.Core/Routing/WebPath.cs, src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs, src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs, tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/WebPathTests.cs
Commit date: 2024-05-17 08:37:51 +0200
Commit message: Merge pull request from GHSA-j74q-mv2c-rxmp


Diff: diff --git a/src/Umbraco.Core/Routing/WebPath.cs b/src/Umbraco.Core/Routing/WebPath.cs
index 7ecafff8a324..47c6b158718d 100644
--- a/src/Umbraco.Core/Routing/WebPath.cs
+++ b/src/Umbraco.Core/Routing/WebPath.cs
@@ -48,6 +48,30 @@ public static string Combine(params string[]? paths)
             }
         }
 
         return sb.ToString();
     }
+
+
+    /// <summary>
+    /// Determines whether the provided web path is well-formed according to the specified UriKind.
+    /// </summary>
+    /// <param name="webPath">The web path to check. This can be null.</param>
+    /// <param name="uriKind">The kind of Uri (Absolute, Relative, or RelativeOrAbsolute).</param>
+    /// <returns>
+    /// true if <paramref name="webPath"/> is well-formed; otherwise, false.
+    /// </returns>
+    public static bool IsWellFormedWebPath(string? webPath, UriKind uriKind)
+    {
+        if (string.IsNullOrWhiteSpace(webPath))
+        {
+            return false;
+        }
+
+        if (webPath.StartsWith("//"))
+        {
+            return uriKind is not UriKind.Relative;
+        }
+
+        return Uri.IsWellFormedUriString(webPath, uriKind);
+    }
 }
diff --git a/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs
index e718696ae31d..71fa9625a6bd 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs
@@ -5,10 +5,11 @@
 using Umbraco.Cms.Core;
 using Umbraco.Cms.Core.Configuration.Models;
 using Umbraco.Cms.Core.IO;
 using Umbraco.Cms.Core.Media;
 using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Routing;
 using Umbraco.Cms.Web.Common.Attributes;
 using Umbraco.Cms.Web.Common.DependencyInjection;
 using Umbraco.Extensions;
 
 namespace Umbraco.Cms.Web.BackOffice.Controllers;
@@ -121,11 +122,11 @@ public IActionResult GetResized(string imagePath, int width)
         return NotFound();
     }
 
     private bool IsAllowed(string encodedImagePath)
     {
-        if(Uri.IsWellFormedUriString(encodedImagePath, UriKind.Relative))
+        if(WebPath.IsWellFormedWebPath(encodedImagePath, UriKind.Relative))
         {
             return true;
         }
 
         var builder = new UriBuilder(encodedImagePath);
diff --git a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs
index 19ca323d9d59..17875c295067 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs
@@ -9,10 +9,11 @@
 using Umbraco.Cms.Core.Hosting;
 using Umbraco.Cms.Core.Models;
 using Umbraco.Cms.Core.Models.Membership;
 using Umbraco.Cms.Core.Models.PublishedContent;
 using Umbraco.Cms.Core.PublishedCache;
+using Umbraco.Cms.Core.Routing;
 using Umbraco.Cms.Core.Security;
 using Umbraco.Cms.Core.Services;
 using Umbraco.Cms.Core.Web;
 using Umbraco.Cms.Core.WebAssets;
 using Umbraco.Cms.Infrastructure.WebAssets;
@@ -150,12 +151,11 @@ public ActionResult End(string? redir = null)
         _cookieManager.ExpireCookie(Constants.Web.PreviewCookieName);
 
         // Expire Client-side cookie that determines whether the user has accepted to be in Preview Mode when visiting the website.
         _cookieManager.ExpireCookie(Constants.Web.AcceptPreviewCookieName);
 
-        if (Uri.IsWellFormedUriString(redir, UriKind.Relative)
-            && redir.StartsWith("//") == false
+        if (WebPath.IsWellFormedWebPath(redir, UriKind.Relative)
             && Uri.TryCreate(redir, UriKind.Relative, out Uri? url))
         {
             return Redirect(url.ToString());
         }
 
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/WebPathTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/WebPathTests.cs
index 098e047981f4..acfa4ffe6fd6 100644
--- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/WebPathTests.cs
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/WebPathTests.cs
@@ -29,6 +29,89 @@ public class WebPathTests
     public void Combine_must_handle_empty_array() =>
         Assert.AreEqual(string.Empty, WebPath.Combine(Array.Empty<string>()));
 
     [Test]
     public void Combine_must_handle_null() => Assert.Throws<ArgumentNullException>(() => WebPath.Combine(null));
+
+
+    [Test]
+    [TestCase("ftp://hello.com/", UriKind.Absolute, ExpectedResult = true)]
+    [TestCase("file:///hello.com/", UriKind.Absolute, ExpectedResult = true)]
+    [TestCase("ws://hello.com/", UriKind.Absolute, ExpectedResult = true)]
+    [TestCase("wss://hello.com/", UriKind.Absolute, ExpectedResult = true)]
+    [TestCase("https://hello.com:8080/", UriKind.Absolute, ExpectedResult = true)]
+    [TestCase("http://hello.com:8080/", UriKind.Absolute, ExpectedResult = true)]
+    [TestCase("https://hello.com/path", UriKind.Absolute, ExpectedResult = true)]
+    [TestCase("http://hello.com/path", UriKind.Absolute, ExpectedResult = true)]
+    [TestCase("https://hello.com/path?query=param", UriKind.Absolute, ExpectedResult = true)]
+    [TestCase("http://hello.com/path?query=param", UriKind.Absolute, ExpectedResult = true)]
+    [TestCase("https://hello.com/path#fragment", UriKind.Absolute, ExpectedResult = true)]
+    [TestCase("http://hello.com/path#fragment", UriKind.Absolute, ExpectedResult = true)]
+    [TestCase("https://hello.com/path?query=param#fragment", UriKind.Absolute, ExpectedResult = true)]
+    [TestCase("http://hello.com/path?query=param#fragment", UriKind.Absolute, ExpectedResult = true)]
+    [TestCase("https://hello.com:8080/path?query=param#fragment", UriKind.Absolute, ExpectedResult = true)]
+    [TestCase("http://hello.com:8080/path?query=param#fragment", UriKind.Absolute, ExpectedResult = true)]
+    [TestCase("//hello.com:8080/path?query=param#fragment", UriKind.Absolute, ExpectedResult = true)]
+    [TestCase("//hello.com:8080/path", UriKind.Absolute, ExpectedResult = true)]
+    [TestCase("//hello.com:8080", UriKind.Absolute, ExpectedResult = true)]
+    [TestCase("//hello.com", UriKind.Absolute, ExpectedResult = true)]
+    [TestCase("/test/test.jpg", UriKind.Absolute, ExpectedResult = false)]
+    [TestCase("/test", UriKind.Absolute, ExpectedResult = false)]
+    [TestCase("test", UriKind.Absolute, ExpectedResult = false)]
+    [TestCase("", UriKind.Absolute, ExpectedResult = false)]
+    [TestCase(null, UriKind.Absolute, ExpectedResult = false)]
+    [TestCase("this is not welformed", UriKind.Absolute, ExpectedResult = false)]
+    [TestCase("ftp://hello.com/", UriKind.Relative, ExpectedResult = false)]
+    [TestCase("file:///hello.com/", UriKind.Relative, ExpectedResult = false)]
+    [TestCase("ws://hello.com/", UriKind.Relative, ExpectedResult = false)]
+    [TestCase("wss://hello.com/", UriKind.Relative, ExpectedResult = false)]
+    [TestCase("https://hello.com:8080/", UriKind.Relative, ExpectedResult = false)]
+    [TestCase("http://hello.com:8080/", UriKind.Relative, ExpectedResult = false)]
+    [TestCase("https://hello.com/path", UriKind.Relative, ExpectedResult = false)]
+    [TestCase("http://hello.com/path", UriKind.Relative, ExpectedResult = false)]
+    [TestCase("https://hello.com/path?query=param", UriKind.Relative, ExpectedResult = false)]
+    [TestCase("http://hello.com/path?query=param", UriKind.Relative, ExpectedResult = false)]
+    [TestCase("https://hello.com/path#fragment", UriKind.Relative, ExpectedResult = false)]
+    [TestCase("http://hello.com/path#fragment", UriKind.Relative, ExpectedResult = false)]
+    [TestCase("https://hello.com/path?query=param#fragment", UriKind.Relative, ExpectedResult = false)]
+    [TestCase("http://hello.com/path?query=param#fragment", UriKind.Relative, ExpectedResult = false)]
+    [TestCase("https://hello.com:8080/path?query=param#fragment", UriKind.Relative, ExpectedResult = false)]
+    [TestCase("http://hello.com:8080/path?query=param#fragment", UriKind.Relative, ExpectedResult = false)]
+    [TestCase("//hello.com:8080/path?query=param#fragment", UriKind.Relative, ExpectedResult = false)]
+    [TestCase("//hello.com:8080/path", UriKind.Relative, ExpectedResult = false)]
+    [TestCase("//hello.com:8080", UriKind.Relative, ExpectedResult = false)]
+    [TestCase("//hello.com", UriKind.Relative, ExpectedResult = false)]
+    [TestCase("/test/test.jpg", UriKind.Relative, ExpectedResult = true)]
+    [TestCase("/test", UriKind.Relative, ExpectedResult = true)]
+    [TestCase("test", UriKind.Relative, ExpectedResult = true)]
+    [TestCase("", UriKind.Relative, ExpectedResult = false)]
+    [TestCase(null, UriKind.Relative, ExpectedResult = false)]
+    [TestCase("this is not welformed", UriKind.Relative, ExpectedResult = false)]
+    [TestCase("ftp://hello.com/", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
+    [TestCase("file:///hello.com/", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
+    [TestCase("ws://hello.com/", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
+    [TestCase("wss://hello.com/", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
+    [TestCase("https://hello.com:8080/", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
+    [TestCase("http://hello.com:8080/", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
+    [TestCase("https://hello.com/path", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
+    [TestCase("http://hello.com/path", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
+    [TestCase("https://hello.com/path?query=param", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
+    [TestCase("http://hello.com/path?query=param", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
+    [TestCase("https://hello.com/path#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
+    [TestCase("http://hello.com/path#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
+    [TestCase("https://hello.com/path?query=param#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
+    [TestCase("http://hello.com/path?query=param#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
+    [TestCase("https://hello.com:8080/path?query=param#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
+    [TestCase("http://hello.com:8080/path?query=param#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
+    [TestCase("//hello.com:8080/path?query=param#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
+    [TestCase("//hello.com:8080/path", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
+    [TestCase("//hello.com:8080", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
+    [TestCase("//hello.com", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
+    [TestCase("/test/test.jpg", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
+    [TestCase("/test", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
+    [TestCase("test", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
+    [TestCase("", UriKind.RelativeOrAbsolute, ExpectedResult = false)]
+    [TestCase(null, UriKind.RelativeOrAbsolute, ExpectedResult = false)]
+    [TestCase("this is not welformed", UriKind.RelativeOrAbsolute, ExpectedResult = false)]
+    public bool IsWellFormedWebPath(string? webPath, UriKind uriKind) => WebPath.IsWellFormedWebPath(webPath, uriKind);
+
 }

Vulnerable code:         if(Uri.IsWellFormedUriString(encodedImagePath, UriKind.Relative))
        if (Uri.IsWellFormedUriString(redir, UriKind.Relative)
            && redir.StartsWith("//") == false
Fixed code: 

    /// <summary>
    /// Determines whether the provided web path is well-formed according to the specified UriKind.
    /// </summary>
    /// <param name="webPath">The web path to check. This can be null.</param>
    /// <param name="uriKind">The kind of Uri (Absolute, Relative, or RelativeOrAbsolute).</param>
    /// <returns>
    /// true if <paramref name="webPath"/> is well-formed; otherwise, false.
    /// </returns>
    public static bool IsWellFormedWebPath(string? webPath, UriKind uriKind)
    {
        if (string.IsNullOrWhiteSpace(webPath))
        {
            return false;
        }

        if (webPath.StartsWith("//"))
        {
            return uriKind is not UriKind.Relative;
        }

        return Uri.IsWellFormedUriString(webPath, uriKind);
    }
using Umbraco.Cms.Core.Routing;
        if(WebPath.IsWellFormedWebPath(encodedImagePath, UriKind.Relative))
using Umbraco.Cms.Core.Routing;
        if (WebPath.IsWellFormedWebPath(redir, UriKind.Relative)


    [Test]
    [TestCase("ftp://hello.com/", UriKind.Absolute, ExpectedResult = true)]
    [TestCase("file:///hello.com/", UriKind.Absolute, ExpectedResult = true)]
    [TestCase("ws://hello.com/", UriKind.Absolute, ExpectedResult = true)]
    [TestCase("wss://hello.com/", UriKind.Absolute, ExpectedResult = true)]
    [TestCase("https://hello.com:8080/", UriKind.Absolute, ExpectedResult = true)]
    [TestCase("http://hello.com:8080/", UriKind.Absolute, ExpectedResult = true)]
    [TestCase("https://hello.com/path", UriKind.Absolute, ExpectedResult = true)]
    [TestCase("http://hello.com/path", UriKind.Absolute, ExpectedResult = true)]
    [TestCase("https://hello.com/path?query=param", UriKind.Absolute, ExpectedResult = true)]
    [TestCase("http://hello.com/path?query=param", UriKind.Absolute, ExpectedResult = true)]
    [TestCase("https://hello.com/path#fragment", UriKind.Absolute, ExpectedResult = true)]
    [TestCase("http://hello.com/path#fragment", UriKind.Absolute, ExpectedResult = true)]
    [TestCase("https://hello.com/path?query=param#fragment", UriKind.Absolute, ExpectedResult = true)]
    [TestCase("http://hello.com/path?query=param#fragment", UriKind.Absolute, ExpectedResult = true)]
    [TestCase("https://hello.com:8080/path?query=param#fragment", UriKind.Absolute, ExpectedResult = true)]
    [TestCase("http://hello.com:8080/path?query=param#fragment", UriKind.Absolute, ExpectedResult = true)]
    [TestCase("//hello.com:8080/path?query=param#fragment", UriKind.Absolute, ExpectedResult = true)]
    [TestCase("//hello.com:8080/path", UriKind.Absolute, ExpectedResult = true)]
    [TestCase("//hello.com:8080", UriKind.Absolute, ExpectedResult = true)]
    [TestCase("//hello.com", UriKind.Absolute, ExpectedResult = true)]
    [TestCase("/test/test.jpg", UriKind.Absolute, ExpectedResult = false)]
    [TestCase("/test", UriKind.Absolute, ExpectedResult = false)]
    [TestCase("test", UriKind.Absolute, ExpectedResult = false)]
    [TestCase("", UriKind.Absolute, ExpectedResult = false)]
    [TestCase(null, UriKind.Absolute, ExpectedResult = false)]
    [TestCase("this is not welformed", UriKind.Absolute, ExpectedResult = false)]
    [TestCase("ftp://hello.com/", UriKind.Relative, ExpectedResult = false)]
    [TestCase("file:///hello.com/", UriKind.Relative, ExpectedResult = false)]
    [TestCase("ws://hello.com/", UriKind.Relative, ExpectedResult = false)]
    [TestCase("wss://hello.com/", UriKind.Relative, ExpectedResult = false)]
    [TestCase("https://hello.com:8080/", UriKind.Relative, ExpectedResult = false)]
    [TestCase("http://hello.com:8080/", UriKind.Relative, ExpectedResult = false)]
    [TestCase("https://hello.com/path", UriKind.Relative, ExpectedResult = false)]
    [TestCase("http://hello.com/path", UriKind.Relative, ExpectedResult = false)]
    [TestCase("https://hello.com/path?query=param", UriKind.Relative, ExpectedResult = false)]
    [TestCase("http://hello.com/path?query=param", UriKind.Relative, ExpectedResult = false)]
    [TestCase("https://hello.com/path#fragment", UriKind.Relative, ExpectedResult = false)]
    [TestCase("http://hello.com/path#fragment", UriKind.Relative, ExpectedResult = false)]
    [TestCase("https://hello.com/path?query=param#fragment", UriKind.Relative, ExpectedResult = false)]
    [TestCase("http://hello.com/path?query=param#fragment", UriKind.Relative, ExpectedResult = false)]
    [TestCase("https://hello.com:8080/path?query=param#fragment", UriKind.Relative, ExpectedResult = false)]
    [TestCase("http://hello.com:8080/path?query=param#fragment", UriKind.Relative, ExpectedResult = false)]
    [TestCase("//hello.com:8080/path?query=param#fragment", UriKind.Relative, ExpectedResult = false)]
    [TestCase("//hello.com:8080/path", UriKind.Relative, ExpectedResult = false)]
    [TestCase("//hello.com:8080", UriKind.Relative, ExpectedResult = false)]
    [TestCase("//hello.com", UriKind.Relative, ExpectedResult = false)]
    [TestCase("/test/test.jpg", UriKind.Relative, ExpectedResult = true)]
    [TestCase("/test", UriKind.Relative, ExpectedResult = true)]
    [TestCase("test", UriKind.Relative, ExpectedResult = true)]
    [TestCase("", UriKind.Relative, ExpectedResult = false)]
    [TestCase(null, UriKind.Relative, ExpectedResult = false)]
    [TestCase("this is not welformed", UriKind.Relative, ExpectedResult = false)]
    [TestCase("ftp://hello.com/", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
    [TestCase("file:///hello.com/", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
    [TestCase("ws://hello.com/", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
    [TestCase("wss://hello.com/", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
    [TestCase("https://hello.com:8080/", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
    [TestCase("http://hello.com:8080/", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
    [TestCase("https://hello.com/path", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
    [TestCase("http://hello.com/path", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
    [TestCase("https://hello.com/path?query=param", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
    [TestCase("http://hello.com/path?query=param", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
    [TestCase("https://hello.com/path#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
    [TestCase("http://hello.com/path#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
    [TestCase("https://hello.com/path?query=param#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
    [TestCase("http://hello.com/path?query=param#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
    [TestCase("https://hello.com:8080/path?query=param#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
    [TestCase("http://hello.com:8080/path?query=param#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
    [TestCase("//hello.com:8080/path?query=param#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
    [TestCase("//hello.com:8080/path", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
    [TestCase("//hello.com:8080", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
    [TestCase("//hello.com", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
    [TestCase("/test/test.jpg", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
    [TestCase("/test", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
    [TestCase("test", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
    [TestCase("", UriKind.RelativeOrAbsolute, ExpectedResult = false)]
    [TestCase(null, UriKind.RelativeOrAbsolute, ExpectedResult = false)]
    [TestCase("this is not welformed", UriKind.RelativeOrAbsolute, ExpectedResult = false)]
    public bool IsWellFormedWebPath(string? webPath, UriKind uriKind) => WebPath.IsWellFormedWebPath(webPath, uriKind);