CVE-2017-0929: Server-Side Request Forgery (SSRF) (CWE-918)
value: DNN (aka DotNetNuke) before 9.2.0 suffers from a Server-Side Request Forgery (SSRF) vulnerability in the DnnImageHandler class. Attackers may be able to access information about internal network resources.
CWE description: The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
CVSS v3 base score: 7.5
Published: 2018-07-03T21:29Z
Repository: https://github.com/dnnsoftware/Dnn.Platform
Language: Other
Files changed: DNN Platform/Library/Services/GeneratedImage/DnnImageHandler.cs	, DNN Platform/Library/Services/GeneratedImage/FilterTransform/ImageResizeTransform.cs	, DNN Platform/Library/Services/GeneratedImage/ImageHandlerInternal.cs	, DNN Platform/Tests/DNN.Integration.Test.Framework/DNN.Integration.Test.Framework.csproj	, DNN Platform/Tests/DNN.Integration.Test.Framework/Helpers/PortalAliasHelper.cs	, DNN Platform/Tests/DotNetNuke.Tests.Integration/DotNetNuke.Tests.Integration.csproj	, DNN Platform/Tests/DotNetNuke.Tests.Integration/Tests/Library/ImageHandlerTests.cs	, DNN Platform/Tests/DotNetNuke.Tests.Integration/Tests/Security/AuthCookieTests.cs	
Commit date: 2018-01-04 17:08:02 -0800
Commit message: DNN-10601: preventing handling external URLs in image handler.

Diff: diff --git a/DNN Platform/Library/Services/GeneratedImage/DnnImageHandler.cs b/DNN Platform/Library/Services/GeneratedImage/DnnImageHandler.cs
index d8562ac1c..8ed6be13c 100644
--- a/DNN Platform/Library/Services/GeneratedImage/DnnImageHandler.cs	
+++ b/DNN Platform/Library/Services/GeneratedImage/DnnImageHandler.cs	
@@ -1,6 +1,7 @@
 ﻿using System;
+using System.Collections.Generic;
 using System.Collections.Specialized;
 using System.Configuration;
 using System.Drawing;
 using System.Drawing.Imaging;
 using System.Globalization;
@@ -231,15 +232,18 @@ public override ImageInfo GenerateImage(NameValueCollection parameters)
                             }
                             imgFile = fullFilePath;
                         }
                         else if (!string.IsNullOrEmpty(parameters["Url"]))
                         {
-                            if (!parameters["Url"].StartsWith("http"))
+                            var url = parameters["Url"];
+                            // allow only site resources when using the url parameter
+                            if (!url.StartsWith("http") || !UriBelongsToSite(new Uri(url)))
                             {
                                 return GetEmptyImageInfo();
                             }
-                            imgUrl = parameters["Url"];
+
+                            imgUrl = url;
                         }
 
                         if (string.IsNullOrEmpty(parameters["format"]))
                         {
                             string extension;
@@ -553,7 +557,22 @@ private static ImageFormat GetImageFormat(string extension)
                     return ImageFormat.Icon;
                 default:
                     return ImageFormat.Png;
             }
         }
+
+        // checks whether the uri belongs to any of the site-wide aliases
+        private static bool UriBelongsToSite(Uri uri)
+        {
+            IEnumerable<string> hostAliases =
+                from PortalAliasInfo alias in PortalAliasController.Instance.GetPortalAliases().Values
+                 select alias.HTTPAlias.ToLowerInvariant();
+
+            // if URI, for example, = "http(s)://myDomain:80/DNNDev/myPage?var=name" , then the two strings will be
+            // uriNoScheme1 = "mydomain/dnndev/mypage"  -- lower case
+            // uriNoScheme2 = "mydomain:80/dnndev/mypage"  -- lower case
+            var uriNoScheme1 = (uri.DnsSafeHost + uri.LocalPath).ToLowerInvariant();
+            var uriNoScheme2 = (uri.Authority + uri.LocalPath).ToLowerInvariant();
+            return hostAliases.Any(alias => uriNoScheme1.StartsWith(alias) || uriNoScheme2.StartsWith(alias));
+        }
     }
 }
\ No newline at end of file
diff --git a/DNN Platform/Library/Services/GeneratedImage/FilterTransform/ImageResizeTransform.cs b/DNN Platform/Library/Services/GeneratedImage/FilterTransform/ImageResizeTransform.cs
index ad8a3b67e..786b072c5 100644
--- a/DNN Platform/Library/Services/GeneratedImage/FilterTransform/ImageResizeTransform.cs	
+++ b/DNN Platform/Library/Services/GeneratedImage/FilterTransform/ImageResizeTransform.cs	
@@ -111,11 +111,14 @@ public int Border
         /// Processes an input image applying a resize image transformation
         /// </summary>
         /// <param name="image">Input image</param>
         /// <returns>Image result after image transformation</returns>
         public override Image ProcessImage(Image image)
-		{
+        {
+            if (image == null)
+                return null;    
+
             if (MaxWidth > 0)
             {
                 Width = image.Width > MaxWidth ? MaxWidth : image.Width;
             }
 
diff --git a/DNN Platform/Library/Services/GeneratedImage/ImageHandlerInternal.cs b/DNN Platform/Library/Services/GeneratedImage/ImageHandlerInternal.cs
index a5a0f2ae2..94528aae7 100644
--- a/DNN Platform/Library/Services/GeneratedImage/ImageHandlerInternal.cs	
+++ b/DNN Platform/Library/Services/GeneratedImage/ImageHandlerInternal.cs	
@@ -424,11 +424,11 @@ private void RenderImage(Image image, Stream outStream)
                     var eps = new EncoderParameters(1)
                     {
                         Param = { [0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, ImageCompression) }
                     };
                     var ici = GetEncoderInfo(GetImageMimeType(ContentType));
-                    image.Save(outStream, ici, eps);
+                    image?.Save(outStream, ici, eps);
                 }
             }
             finally
             {
                 image?.Dispose();
diff --git a/DNN Platform/Tests/DNN.Integration.Test.Framework/DNN.Integration.Test.Framework.csproj b/DNN Platform/Tests/DNN.Integration.Test.Framework/DNN.Integration.Test.Framework.csproj
index 2499a5340..b15984444 100644
--- a/DNN Platform/Tests/DNN.Integration.Test.Framework/DNN.Integration.Test.Framework.csproj	
+++ b/DNN Platform/Tests/DNN.Integration.Test.Framework/DNN.Integration.Test.Framework.csproj	
@@ -79,10 +79,11 @@
     <Compile Include="Helpers\DnnDataHelper.cs" />
     <Compile Include="Helpers\FileHelper.cs" />
     <Compile Include="Helpers\FolderHelper.cs" />
     <Compile Include="Helpers\HostSettingsHelper.cs" />
     <Compile Include="Helpers\MockComponentHelper.cs" />
+    <Compile Include="Helpers\PortalAliasHelper.cs" />
     <Compile Include="Helpers\PortalInfoHelper.cs" />
     <Compile Include="Helpers\PortalSettingsHelper.cs" />
     <Compile Include="Helpers\StoredProcedureStatsHelper.cs" />
     <Compile Include="Helpers\WebApiTestHelper.cs" />
     <Compile Include="IntegrationConstants.cs" />
diff --git a/DNN Platform/Tests/DNN.Integration.Test.Framework/Helpers/PortalAliasHelper.cs b/DNN Platform/Tests/DNN.Integration.Test.Framework/Helpers/PortalAliasHelper.cs
new file mode 100644
index 000000000..615da9c1b
--- /dev/null
+++ b/DNN Platform/Tests/DNN.Integration.Test.Framework/Helpers/PortalAliasHelper.cs	
@@ -0,0 +1,40 @@
+﻿// DotNetNuke® - http://www.dnnsoftware.com
+// Copyright (c) 2002-2018, DNN Corp.
+// All Rights Reserved
+
+using System.Collections.Generic;
+using System.Linq;
+using DotNetNuke.Entities.Portals;
+
+namespace DNN.Integration.Test.Framework.Helpers
+{
+    public class PortalAliasHelper
+    {
+        public static PortalAliasInfo GetPrimaryPortalAlias(int portalId = 0)
+        {
+            return DatabaseHelper.ExecuteStoredProcedure<PortalAliasInfo>("GetPortalAliases").First(p => p.PortalID == portalId && p.IsPrimary);
+        }
+
+        public static IEnumerable<PortalAliasInfo> GetPortalAliaes(int portalId = 0)
+        {
+            return DatabaseHelper.ExecuteStoredProcedure<PortalAliasInfo>("GetPortalAliases").Where(p => p.PortalID == portalId);
+        }
+
+        public static IEnumerable<PortalAliasInfo> GetAllPortalAliaes()
+        {
+            return DatabaseHelper.ExecuteStoredProcedure<PortalAliasInfo>("GetPortalAliases");
+        }
+
+        public static void AddPortalAlias(PortalAliasInfo portal)
+        {
+            DatabaseHelper.ExecuteStoredProcedure("AddPortalAlias",
+                portal.PortalID,
+                portal.HTTPAlias,
+                portal.CultureCode,
+                portal.Skin,
+                portal.BrowserType,
+                portal.IsPrimary,
+                -1);
+        }
+    }
+}
diff --git a/DNN Platform/Tests/DotNetNuke.Tests.Integration/DotNetNuke.Tests.Integration.csproj b/DNN Platform/Tests/DotNetNuke.Tests.Integration/DotNetNuke.Tests.Integration.csproj
index b47625bbb..7e7274f38 100644
--- a/DNN Platform/Tests/DotNetNuke.Tests.Integration/DotNetNuke.Tests.Integration.csproj	
+++ b/DNN Platform/Tests/DotNetNuke.Tests.Integration/DotNetNuke.Tests.Integration.csproj	
@@ -96,10 +96,11 @@
   </ItemGroup>
   <ItemGroup>
     <Compile Include="PersonaBar\Content\Pages\AddPageTests.cs" />
     <Compile Include="PersonaBar\Content\Pages\PageUrlsTests.cs" />
     <Compile Include="PersonaBar\Manage\Users\UsersFiltersTests.cs" />
+    <Compile Include="Tests\Library\ImageHandlerTests.cs" />
     <Compile Include="Tests\Portals\PortalInfoTests.cs" />
     <Compile Include="Tests\Portals\PortalSettingsTests.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="Executers\Builders\PageSettingsBuilder.cs" />
     <Compile Include="Executers\Builders\TabPermissionsBuilder.cs" />
diff --git a/DNN Platform/Tests/DotNetNuke.Tests.Integration/Tests/Library/ImageHandlerTests.cs b/DNN Platform/Tests/DotNetNuke.Tests.Integration/Tests/Library/ImageHandlerTests.cs
new file mode 100644
index 000000000..32bc2811a
--- /dev/null
+++ b/DNN Platform/Tests/DotNetNuke.Tests.Integration/Tests/Library/ImageHandlerTests.cs	
@@ -0,0 +1,76 @@
+﻿// DotNetNuke® - http://www.dnnsoftware.com
+// Copyright (c) 2002-2018, DNN Corp.
+// All Rights Reserved
+
+using System.Linq;
+using DNN.Integration.Test.Framework;
+using DNN.Integration.Test.Framework.Helpers;
+using DotNetNuke.Entities.Portals;
+using NUnit.Framework;
+
+namespace DotNetNuke.Tests.Integration.Tests.Library
+{
+    [TestFixture]
+    public class ImageHandlerTests : IntegrationTestBase
+    {
+        // we assume this GIF image exists in all DNN sites and it shouldn't be PNG
+        private const string HandlerPath = "/DnnImageHandler.ashx?mode=file&w=1&url={0}/images/1x1.GIF";
+
+        [Test]
+        public void Using_Image_Handler_For_Foreign_Site_ShouldFail()
+        {
+            var session = WebApiTestHelper.GetAnnonymousConnector();
+            var relativeUrl = string.Format(HandlerPath, "https://google.com");
+
+            var response = session.GetContent(relativeUrl).Content.ReadAsStringAsync().Result;
+            Assert.IsTrue(response.StartsWith("�PNG\r\n"), $"Content = {response}");
+        }
+
+        [Test]
+        public void Using_Image_Handler_For_Main_Alias_ShouldPass()
+        {
+            var session = WebApiTestHelper.GetAnnonymousConnector();
+            var relativeUrl = string.Format(HandlerPath, AppConfigHelper.SiteUrl);
+
+            var response = session.GetContent(relativeUrl).Content.ReadAsStringAsync().Result;
+            Assert.IsTrue(response.StartsWith("GIF89a"), $"Content = {response}");
+        }
+
+        [Test]
+        public void Using_Image_Handler_From_All_Alias_ShouldPass()
+        {
+            PortalAliasInfo pai;
+            var aliases = PortalAliasHelper.GetPortalAliaes().ToList();
+            if (aliases.Count == 1)
+            {
+                var primary = aliases.First();
+                pai = new PortalAliasInfo
+                {
+                    PortalID = primary.PortalID,
+                    HTTPAlias = "my-" + primary.HTTPAlias,
+                    CultureCode = primary.CultureCode,
+                    Skin = primary.Skin,
+                    BrowserType = primary.BrowserType,
+                    IsPrimary = false,
+
+                };
+            }
+            else
+            {
+                pai = aliases.First(a => a.PortalAliasID > 1);
+            }
+
+            var session = WebApiTestHelper.GetAnnonymousConnector();
+            aliases = PortalAliasHelper.GetPortalAliaes().ToList();
+            foreach (var alias in aliases)
+            {
+                var relativeUrl = string.Format(HandlerPath, AppConfigHelper.SiteUrl);
+                var absoluteUrl = $"http://{alias.HTTPAlias}{relativeUrl}";
+                LogText("Getting image from " + absoluteUrl);
+
+                var response = session.GetContent(absoluteUrl).Content.ReadAsStringAsync().Result;
+                Assert.IsTrue(response.StartsWith("GIF89a"), $"Url: {absoluteUrl} / Content = {response}");
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/DNN Platform/Tests/DotNetNuke.Tests.Integration/Tests/Security/AuthCookieTests.cs b/DNN Platform/Tests/DotNetNuke.Tests.Integration/Tests/Security/AuthCookieTests.cs
index 720d39116..223159ece 100644
--- a/DNN Platform/Tests/DotNetNuke.Tests.Integration/Tests/Security/AuthCookieTests.cs	
+++ b/DNN Platform/Tests/DotNetNuke.Tests.Integration/Tests/Security/AuthCookieTests.cs	
@@ -1,10 +1,12 @@
-﻿using System;
+﻿// DotNetNuke® - http://www.dnnsoftware.com
+// Copyright (c) 2002-2018, DNN Corp.
+// All Rights Reserved
+
+using System;
 using System.Net;
 using System.Net.Http;
-using System.Net.Http.Headers;
-using System.Threading;
 using DNN.Integration.Test.Framework;
 using DNN.Integration.Test.Framework.Helpers;
 using NUnit.Framework;
 
 namespace DotNetNuke.Tests.Integration.Tests.Security

Vulnerable code:                             if (!parameters["Url"].StartsWith("http"))
                            imgUrl = parameters["Url"];
		{
                    image.Save(outStream, ici, eps);
﻿using System;
using System.Net.Http.Headers;
using System.Threading;
Fixed code: using System.Collections.Generic;
                            var url = parameters["Url"];
                            // allow only site resources when using the url parameter
                            if (!url.StartsWith("http") || !UriBelongsToSite(new Uri(url)))

                            imgUrl = url;

        // checks whether the uri belongs to any of the site-wide aliases
        private static bool UriBelongsToSite(Uri uri)
        {
            IEnumerable<string> hostAliases =
                from PortalAliasInfo alias in PortalAliasController.Instance.GetPortalAliases().Values
                 select alias.HTTPAlias.ToLowerInvariant();

            // if URI, for example, = "http(s)://myDomain:80/DNNDev/myPage?var=name" , then the two strings will be
            // uriNoScheme1 = "mydomain/dnndev/mypage"  -- lower case
            // uriNoScheme2 = "mydomain:80/dnndev/mypage"  -- lower case
            var uriNoScheme1 = (uri.DnsSafeHost + uri.LocalPath).ToLowerInvariant();
            var uriNoScheme2 = (uri.Authority + uri.LocalPath).ToLowerInvariant();
            return hostAliases.Any(alias => uriNoScheme1.StartsWith(alias) || uriNoScheme2.StartsWith(alias));
        }
        {
            if (image == null)
                return null;    

                    image?.Save(outStream, ici, eps);
    <Compile Include="Helpers\PortalAliasHelper.cs" />
﻿// DotNetNuke® - http://www.dnnsoftware.com
// Copyright (c) 2002-2018, DNN Corp.
// All Rights Reserved

using System.Collections.Generic;
using System.Linq;
using DotNetNuke.Entities.Portals;

namespace DNN.Integration.Test.Framework.Helpers
{
    public class PortalAliasHelper
    {
        public static PortalAliasInfo GetPrimaryPortalAlias(int portalId = 0)
        {
            return DatabaseHelper.ExecuteStoredProcedure<PortalAliasInfo>("GetPortalAliases").First(p => p.PortalID == portalId && p.IsPrimary);
        }

        public static IEnumerable<PortalAliasInfo> GetPortalAliaes(int portalId = 0)
        {
            return DatabaseHelper.ExecuteStoredProcedure<PortalAliasInfo>("GetPortalAliases").Where(p => p.PortalID == portalId);
        }

        public static IEnumerable<PortalAliasInfo> GetAllPortalAliaes()
        {
            return DatabaseHelper.ExecuteStoredProcedure<PortalAliasInfo>("GetPortalAliases");
        }

        public static void AddPortalAlias(PortalAliasInfo portal)
        {
            DatabaseHelper.ExecuteStoredProcedure("AddPortalAlias",
                portal.PortalID,
                portal.HTTPAlias,
                portal.CultureCode,
                portal.Skin,
                portal.BrowserType,
                portal.IsPrimary,
                -1);
        }
    }
}
    <Compile Include="Tests\Library\ImageHandlerTests.cs" />
﻿// DotNetNuke® - http://www.dnnsoftware.com
// Copyright (c) 2002-2018, DNN Corp.
// All Rights Reserved

using System.Linq;
using DNN.Integration.Test.Framework;
using DNN.Integration.Test.Framework.Helpers;
using DotNetNuke.Entities.Portals;
using NUnit.Framework;

namespace DotNetNuke.Tests.Integration.Tests.Library
{
    [TestFixture]
    public class ImageHandlerTests : IntegrationTestBase
    {
        // we assume this GIF image exists in all DNN sites and it shouldn't be PNG
        private const string HandlerPath = "/DnnImageHandler.ashx?mode=file&w=1&url={0}/images/1x1.GIF";

        [Test]
        public void Using_Image_Handler_For_Foreign_Site_ShouldFail()
        {
            var session = WebApiTestHelper.GetAnnonymousConnector();
            var relativeUrl = string.Format(HandlerPath, "https://google.com");

            var response = session.GetContent(relativeUrl).Content.ReadAsStringAsync().Result;
            Assert.IsTrue(response.StartsWith("�PNG\r\n"), $"Content = {response}");
        }

        [Test]
        public void Using_Image_Handler_For_Main_Alias_ShouldPass()
        {
            var session = WebApiTestHelper.GetAnnonymousConnector();
            var relativeUrl = string.Format(HandlerPath, AppConfigHelper.SiteUrl);

            var response = session.GetContent(relativeUrl).Content.ReadAsStringAsync().Result;
            Assert.IsTrue(response.StartsWith("GIF89a"), $"Content = {response}");
        }

        [Test]
        public void Using_Image_Handler_From_All_Alias_ShouldPass()
        {
            PortalAliasInfo pai;
            var aliases = PortalAliasHelper.GetPortalAliaes().ToList();
            if (aliases.Count == 1)
            {
                var primary = aliases.First();
                pai = new PortalAliasInfo
                {
                    PortalID = primary.PortalID,
                    HTTPAlias = "my-" + primary.HTTPAlias,
                    CultureCode = primary.CultureCode,
                    Skin = primary.Skin,
                    BrowserType = primary.BrowserType,
                    IsPrimary = false,

                };
            }
            else
            {
                pai = aliases.First(a => a.PortalAliasID > 1);
            }

            var session = WebApiTestHelper.GetAnnonymousConnector();
            aliases = PortalAliasHelper.GetPortalAliaes().ToList();
            foreach (var alias in aliases)
            {
                var relativeUrl = string.Format(HandlerPath, AppConfigHelper.SiteUrl);
                var absoluteUrl = $"http://{alias.HTTPAlias}{relativeUrl}";
                LogText("Getting image from " + absoluteUrl);

                var response = session.GetContent(absoluteUrl).Content.ReadAsStringAsync().Result;
                Assert.IsTrue(response.StartsWith("GIF89a"), $"Url: {absoluteUrl} / Content = {response}");
            }
        }
    }
}
﻿// DotNetNuke® - http://www.dnnsoftware.com
// Copyright (c) 2002-2018, DNN Corp.
// All Rights Reserved

using System;