Skip to content

Commit

Permalink
Hardened handling of DnnImageHandler url validation
Browse files Browse the repository at this point in the history
Hardened handling of DnnImageHandler url validation
  • Loading branch information
valadas committed Dec 3, 2024
1 parent 36560af commit 79870bb
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 16 deletions.
1 change: 1 addition & 0 deletions DNN Platform/Library/DotNetNuke.Library.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,7 @@
<Compile Include="Services\GeneratedImage\StartTransform\PlaceHolderTransform.cs" />
<Compile Include="Services\GeneratedImage\StartTransform\SecureFileTransform.cs" />
<Compile Include="Services\GeneratedImage\StartTransform\UserProfilePicTransform.cs" />
<Compile Include="Services\GeneratedImage\UriValidator.cs" />
<Compile Include="Services\Installer\Blocker\IInstallBlocker.cs" />
<Compile Include="Services\Installer\Blocker\InstallBlocker.cs" />
<Compile Include="Services\Installer\Dependencies\IManagedPackageDependency.cs" />
Expand Down
19 changes: 3 additions & 16 deletions DNN Platform/Library/Services/GeneratedImage/DnnImageHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,9 @@ public override ImageInfo GenerateImage(NameValueCollection parameters)
var url = parameters["Url"];

// allow only site resources when using the url parameter
if (!url.StartsWith("http") || !UriBelongsToSite(new Uri(url)))
IPortalAliasController portalAliasController = PortalAliasController.Instance;
var uriValidator = new UriValidator(portalAliasController);
if (!url.StartsWith("http") || !uriValidator.UriBelongsToSite(new Uri(url)))
{
return this.GetEmptyImageInfo();
}
Expand Down Expand Up @@ -528,21 +530,6 @@ private static ImageFormat GetImageFormat(string extension)
}
}

// 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));
}

private ImageInfo GetEmptyImageInfo()
{
return new ImageInfo(this.EmptyImage)
Expand Down
65 changes: 65 additions & 0 deletions DNN Platform/Library/Services/GeneratedImage/UriValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information

namespace DotNetNuke.Services.GeneratedImage
{
using System;
using System.Collections.Generic;
using System.Linq;

using DotNetNuke.Abstractions.Portals;
using DotNetNuke.Entities.Portals;

/// <summary>
/// Validates urls that could be used in the Image Handler.
/// </summary>
internal class UriValidator
{
private readonly IPortalAliasController portalAliasController;

/// <summary>
/// Initializes a new instance of the <see cref="UriValidator"/> class.
/// </summary>
/// <param name="portalAliasController">Provides services related to portal aliases.</param>
public UriValidator(IPortalAliasController portalAliasController)
{
this.portalAliasController = portalAliasController;
}

/// <summary>
/// Checks if a URI belongs to hosted sites.
/// </summary>
/// <param name="uri">The URI to validate.</param>
/// <returns>A value indicating whether the provided Uri belongs to the a valid site.</returns>
internal bool UriBelongsToSite(Uri uri)
{
IEnumerable<string> hostAliases =
this.portalAliasController
.GetPortalAliases().Values.Cast<IPortalAliasInfo>()
.Select(alias => alias.HttpAlias.ToLowerInvariant());

// Extract the host and normalize the path from the incoming URI
string uriHost = uri.DnsSafeHost.ToLowerInvariant(); // Just the host (e.g., "mysite.com")
string uriPath = uri.LocalPath.TrimEnd('/').ToLowerInvariant(); // Path (e.g., "/siteB")

// Split the alias into host and optional path (e.g., "mysite.com/siteB")
foreach (var alias in hostAliases)
{
var aliasParts = alias.Split(new[] { '/' }, 2, StringSplitOptions.None); // Split on the first '/' to separate host and path
string aliasHost = aliasParts[0]; // Host part of the alias (e.g., "mysite.com")
string aliasPath = aliasParts.Length > 1 ? "/" + aliasParts[1].TrimEnd('/') : string.Empty; // Path part, if any

// Ensure exact host match and validate the path
if (string.Equals(uriHost, aliasHost, StringComparison.OrdinalIgnoreCase) &&
uriPath.StartsWith(aliasPath, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}

// No matching alias found
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@
<Compile Include="Services\ClientCapability\TestClientCapability.cs" />
<Compile Include="Services\CryptographyProviders\FipsCompilanceCryptographyProviderTests.cs" />
<Compile Include="Services\CryptographyProviders\CoreCryptographyProviderTests.cs" />
<Compile Include="Services\GeneratedImage\UriValidatorTests.cs" />
<Compile Include="Services\Installer\AssemblyInstallerTests.cs" />
<Compile Include="Services\Installer\CleanupInstallerTests.cs" />
<Compile Include="Services\Localization\LocalizationTests.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information

namespace DotNetNuke.Tests.Core.Services.GeneratedImage
{
using System;
using DotNetNuke.Entities.Portals;
using DotNetNuke.Services.GeneratedImage;
using Moq;
using NUnit.Framework;

[TestFixture]
public class UriValidatorTests
{
[TestCase("https://mysite.com/page", true)]
[TestCase("http://mysite.com/page", true)]
[TestCase("https://mysite.com", true)]
[TestCase("http://mysite.com", true)]
[TestCase("https://mysite.com/siteB", true)]
[TestCase("http://mysite.com/siteB", true)]
[TestCase("https://badactor.com", false)]
[TestCase("http://badactor.com", false)]
[TestCase("https://badactor.com/siteB", false)]
[TestCase("http://badactor.com/siteB", false)]
[TestCase("https://mysite.com.badactor.com", false)]
[TestCase("http://mysite.com.badactor.com", false)]
[TestCase("https://mysite.com.badactor.com/siteB", false)]
[TestCase("http://mysite.com.badactor.com/siteB", false)]
[TestCase("https://mysite.com.badactor.com/siteB/page", false)]
[TestCase("http://mysite.com.badactor.com/siteB/page", false)]
public void UriBelongsToSite_MultipleScenarios(string uriString, bool expected)
{
// Arrange
var mockPortalAliasController = new Mock<IPortalAliasController>();
var portalAliases = new PortalAliasCollection();
portalAliases.Add("mysite", new PortalAliasInfo { HTTPAlias = "mysite.com" });
portalAliases.Add("siteB", new PortalAliasInfo { HTTPAlias = "mysite.com/siteB" });
mockPortalAliasController
.Setup(controller => controller.GetPortalAliases())
.Returns(portalAliases);

var validator = new UriValidator(mockPortalAliasController.Object);

var testUri = new Uri(uriString);

// Act
var result = validator.UriBelongsToSite(testUri);

Assert.That(result, Is.EqualTo(expected));
}
}
}

0 comments on commit 79870bb

Please sign in to comment.