Skip to content

Commit c4fdf80

Browse files
committed
Fixes 10730 - Route hijacking with public access
1 parent 1b0911f commit c4fdf80

File tree

6 files changed

+76
-53
lines changed

6 files changed

+76
-53
lines changed

src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformerTests.cs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ private UmbracoRouteValueTransformer GetTransformer(
4848
IPublishedRouter router = null,
4949
IUmbracoRouteValuesFactory routeValuesFactory = null)
5050
{
51+
var publicAccessRequestHandler = new Mock<IPublicAccessRequestHandler>();
52+
publicAccessRequestHandler.Setup(x => x.RewriteForPublishedContentAccessAsync(It.IsAny<HttpContext>(), It.IsAny<UmbracoRouteValues>()))
53+
.Returns((HttpContext ctx, UmbracoRouteValues routeVals) => Task.FromResult(routeVals));
54+
5155
var transformer = new UmbracoRouteValueTransformer(
5256
new NullLogger<UmbracoRouteValueTransformer>(),
5357
ctx,
@@ -59,7 +63,8 @@ private UmbracoRouteValueTransformer GetTransformer(
5963
filter ?? Mock.Of<IRoutableDocumentFilter>(x => x.IsDocumentRequest(It.IsAny<string>()) == true),
6064
Mock.Of<IDataProtectionProvider>(),
6165
Mock.Of<IControllerActionSearcher>(),
62-
Mock.Of<IEventAggregator>());
66+
Mock.Of<IEventAggregator>(),
67+
publicAccessRequestHandler.Object);
6368

6469
return transformer;
6570
}
@@ -155,7 +160,7 @@ public async Task Assigns_PublishedRequest_To_UmbracoContext()
155160
}
156161

157162
[Test]
158-
public async Task Assigns_UmbracoRouteValues_To_HttpContext_Feature()
163+
public async Task Null_When_No_Content_On_PublishedRequest()
159164
{
160165
IUmbracoContext umbracoContext = GetUmbracoContext(true);
161166
IPublishedRequest request = Mock.Of<IPublishedRequest>();
@@ -168,6 +173,24 @@ public async Task Assigns_UmbracoRouteValues_To_HttpContext_Feature()
168173
var httpContext = new DefaultHttpContext();
169174
RouteValueDictionary result = await transformer.TransformAsync(httpContext, new RouteValueDictionary());
170175

176+
UmbracoRouteValues routeVals = httpContext.Features.Get<UmbracoRouteValues>();
177+
Assert.IsNull(routeVals);
178+
}
179+
180+
[Test]
181+
public async Task Assigns_UmbracoRouteValues_To_HttpContext_Feature()
182+
{
183+
IUmbracoContext umbracoContext = GetUmbracoContext(true);
184+
IPublishedRequest request = Mock.Of<IPublishedRequest>(x => x.PublishedContent == Mock.Of<IPublishedContent>());
185+
186+
UmbracoRouteValueTransformer transformer = GetTransformerWithRunState(
187+
Mock.Of<IUmbracoContextAccessor>(x => x.TryGetUmbracoContext(out umbracoContext)),
188+
router: GetRouter(request),
189+
routeValuesFactory: GetRouteValuesFactory(request));
190+
191+
var httpContext = new DefaultHttpContext();
192+
RouteValueDictionary result = await transformer.TransformAsync(httpContext, new RouteValueDictionary());
193+
171194
UmbracoRouteValues routeVals = httpContext.Features.Get<UmbracoRouteValues>();
172195
Assert.IsNotNull(routeVals);
173196
Assert.AreEqual(routeVals.PublishedRequest, umbracoContext.PublishedRequest);

src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88
using Umbraco.Cms.Web.Common.Middleware;
99
using Umbraco.Cms.Web.Common.Routing;
1010
using Umbraco.Cms.Web.Website.Collections;
11-
using Umbraco.Cms.Web.Website.Controllers;
12-
using Umbraco.Cms.Web.Website.Middleware;
1311
using Umbraco.Cms.Web.Website.Models;
1412
using Umbraco.Cms.Web.Website.Routing;
1513
using Umbraco.Cms.Web.Website.ViewEngines;
@@ -51,7 +49,7 @@ public static IUmbracoBuilder AddWebsite(this IUmbracoBuilder builder)
5149

5250
builder.Services.AddSingleton<MemberModelBuilderFactory>();
5351

54-
builder.Services.AddSingleton<PublicAccessMiddleware>();
52+
builder.Services.AddSingleton<IPublicAccessRequestHandler, PublicAccessRequestHandler>();
5553
builder.Services.AddSingleton<BasicAuthenticationMiddleware>();
5654

5755
builder

src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
using Microsoft.Extensions.DependencyInjection;
44
using Umbraco.Cms.Web.Common.ApplicationBuilder;
55
using Umbraco.Cms.Web.Common.Middleware;
6-
using Umbraco.Cms.Web.Website.Middleware;
76
using Umbraco.Cms.Web.Website.Routing;
87

98
namespace Umbraco.Extensions
@@ -20,7 +19,6 @@ public static partial class UmbracoApplicationBuilderExtensions
2019
/// <returns></returns>
2120
public static IUmbracoApplicationBuilderContext UseWebsite(this IUmbracoApplicationBuilderContext builder)
2221
{
23-
builder.AppBuilder.UseMiddleware<PublicAccessMiddleware>();
2422
builder.AppBuilder.UseMiddleware<BasicAuthenticationMiddleware>();
2523
return builder;
2624
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System.Threading.Tasks;
2+
using Microsoft.AspNetCore.Http;
3+
using Umbraco.Cms.Web.Common.Routing;
4+
5+
namespace Umbraco.Cms.Web.Website.Routing
6+
{
7+
public interface IPublicAccessRequestHandler
8+
{
9+
/// <summary>
10+
/// Ensures that access to current node is permitted.
11+
/// </summary>
12+
/// <param name="httpContext"></param>
13+
/// <param name="routeValues">The current route values</param>
14+
/// <returns>Updated route values if public access changes the rendered content, else the original route values.</returns>
15+
/// <remarks>Redirecting to a different site root and/or culture will not pick the new site root nor the new culture.</remarks>
16+
Task<UmbracoRouteValues> RewriteForPublishedContentAccessAsync(HttpContext httpContext, UmbracoRouteValues routeValues);
17+
}
18+
}

src/Umbraco.Web.Website/Middleware/PublicAccessMiddleware.cs renamed to src/Umbraco.Web.Website/Routing/PublicAccessRequestHandler.cs

Lines changed: 13 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,21 @@
1010
using Umbraco.Cms.Core.Services;
1111
using Umbraco.Cms.Core.Web;
1212
using Umbraco.Cms.Web.Common.Routing;
13-
using Umbraco.Cms.Web.Website.Routing;
1413
using Umbraco.Extensions;
1514

16-
namespace Umbraco.Cms.Web.Website.Middleware
15+
namespace Umbraco.Cms.Web.Website.Routing
1716
{
18-
public class PublicAccessMiddleware : IMiddleware
17+
public class PublicAccessRequestHandler : IPublicAccessRequestHandler
1918
{
20-
private readonly ILogger<PublicAccessMiddleware> _logger;
19+
private readonly ILogger<PublicAccessRequestHandler> _logger;
2120
private readonly IPublicAccessService _publicAccessService;
2221
private readonly IPublicAccessChecker _publicAccessChecker;
2322
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
2423
private readonly IUmbracoRouteValuesFactory _umbracoRouteValuesFactory;
2524
private readonly IPublishedRouter _publishedRouter;
2625

27-
public PublicAccessMiddleware(
28-
ILogger<PublicAccessMiddleware> logger,
26+
public PublicAccessRequestHandler(
27+
ILogger<PublicAccessRequestHandler> logger,
2928
IPublicAccessService publicAccessService,
3029
IPublicAccessChecker publicAccessChecker,
3130
IUmbracoContextAccessor umbracoContextAccessor,
@@ -40,37 +39,22 @@ public PublicAccessMiddleware(
4039
_publishedRouter = publishedRouter;
4140
}
4241

43-
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
44-
{
45-
UmbracoRouteValues umbracoRouteValues = context.Features.Get<UmbracoRouteValues>();
46-
47-
if (umbracoRouteValues != null)
48-
{
49-
await EnsurePublishedContentAccess(context, umbracoRouteValues);
50-
}
51-
52-
await next(context);
53-
}
54-
55-
/// <summary>
56-
/// Ensures that access to current node is permitted.
57-
/// </summary>
58-
/// <remarks>Redirecting to a different site root and/or culture will not pick the new site root nor the new culture.</remarks>
59-
private async Task EnsurePublishedContentAccess(HttpContext httpContext, UmbracoRouteValues routeValues)
42+
/// <inheritdoc />
43+
public async Task<UmbracoRouteValues> RewriteForPublishedContentAccessAsync(HttpContext httpContext, UmbracoRouteValues routeValues)
6044
{
6145
// because these might loop, we have to have some sort of infinite loop detection
6246
int i = 0;
6347
const int maxLoop = 8;
6448
PublicAccessStatus publicAccessStatus = PublicAccessStatus.AccessAccepted;
6549
do
6650
{
67-
_logger.LogDebug(nameof(EnsurePublishedContentAccess) + ": Loop {LoopCounter}", i);
51+
_logger.LogDebug(nameof(RewriteForPublishedContentAccessAsync) + ": Loop {LoopCounter}", i);
6852

6953

7054
IPublishedContent publishedContent = routeValues.PublishedRequest?.PublishedContent;
7155
if (publishedContent == null)
7256
{
73-
return;
57+
return routeValues;
7458
}
7559

7660
var path = publishedContent.Path;
@@ -117,8 +101,10 @@ private async Task EnsurePublishedContentAccess(HttpContext httpContext, Umbraco
117101

118102
if (i == maxLoop)
119103
{
120-
_logger.LogDebug(nameof(EnsurePublishedContentAccess) + ": Looks like we are running into an infinite loop, abort");
104+
_logger.LogDebug(nameof(RewriteForPublishedContentAccessAsync) + ": Looks like we are running into an infinite loop, abort");
121105
}
106+
107+
return routeValues;
122108
}
123109

124110

@@ -139,17 +125,11 @@ private async Task<UmbracoRouteValues> SetPublishedContentAsOtherPageAsync(HttpC
139125
// we need to change the content item that is getting rendered so we have to re-create UmbracoRouteValues.
140126
UmbracoRouteValues updatedRouteValues = await _umbracoRouteValuesFactory.CreateAsync(httpContext, reRouted);
141127

142-
// Update the feature
143-
httpContext.Features.Set(updatedRouteValues);
144-
145128
return updatedRouteValues;
146129
}
147130
else
148131
{
149-
_logger.LogWarning("Public Access rule has a redirect node set to itself, nothing can be routed.");
150-
// Update the feature to nothing - cannot continue
151-
httpContext.Features.Set<UmbracoRouteValues>(null);
152-
return null;
132+
throw new InvalidOperationException("Public Access rule has a redirect node set to itself, nothing can be routed.");
153133
}
154134
}
155135
}

src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public class UmbracoRouteValueTransformer : DynamicRouteValueTransformer
5252
private readonly IDataProtectionProvider _dataProtectionProvider;
5353
private readonly IControllerActionSearcher _controllerActionSearcher;
5454
private readonly IEventAggregator _eventAggregator;
55+
private readonly IPublicAccessRequestHandler _publicAccessRequestHandler;
5556

5657
/// <summary>
5758
/// Initializes a new instance of the <see cref="UmbracoRouteValueTransformer"/> class.
@@ -67,7 +68,8 @@ public UmbracoRouteValueTransformer(
6768
IRoutableDocumentFilter routableDocumentFilter,
6869
IDataProtectionProvider dataProtectionProvider,
6970
IControllerActionSearcher controllerActionSearcher,
70-
IEventAggregator eventAggregator)
71+
IEventAggregator eventAggregator,
72+
IPublicAccessRequestHandler publicAccessRequestHandler)
7173
{
7274
if (globalSettings is null)
7375
{
@@ -85,6 +87,7 @@ public UmbracoRouteValueTransformer(
8587
_dataProtectionProvider = dataProtectionProvider;
8688
_controllerActionSearcher = controllerActionSearcher;
8789
_eventAggregator = eventAggregator;
90+
_publicAccessRequestHandler = publicAccessRequestHandler;
8891
}
8992

9093
/// <inheritdoc/>
@@ -125,20 +128,10 @@ public override async ValueTask<RouteValueDictionary> TransformAsync(HttpContext
125128
};
126129
}
127130

128-
IPublishedRequest publishedRequest = await RouteRequestAsync(httpContext, umbracoContext);
131+
IPublishedRequest publishedRequest = await RouteRequestAsync(umbracoContext);
129132

130133
umbracoRouteValues = await _routeValuesFactory.CreateAsync(httpContext, publishedRequest);
131134

132-
// Store the route values as a httpcontext feature
133-
httpContext.Features.Set(umbracoRouteValues);
134-
135-
// Need to check if there is form data being posted back to an Umbraco URL
136-
PostedDataProxyInfo postedInfo = GetFormInfo(httpContext, values);
137-
if (postedInfo != null)
138-
{
139-
return HandlePostedValues(postedInfo, httpContext);
140-
}
141-
142135
if (!umbracoRouteValues?.PublishedRequest?.HasPublishedContent() ?? false)
143136
{
144137
// No content was found, not by any registered 404 handlers and
@@ -149,6 +142,19 @@ public override async ValueTask<RouteValueDictionary> TransformAsync(HttpContext
149142
return null;
150143
}
151144

145+
// now we need to do some public access checks
146+
umbracoRouteValues = await _publicAccessRequestHandler.RewriteForPublishedContentAccessAsync(httpContext, umbracoRouteValues);
147+
148+
// Store the route values as a httpcontext feature
149+
httpContext.Features.Set(umbracoRouteValues);
150+
151+
// Need to check if there is form data being posted back to an Umbraco URL
152+
PostedDataProxyInfo postedInfo = GetFormInfo(httpContext, values);
153+
if (postedInfo != null)
154+
{
155+
return HandlePostedValues(postedInfo, httpContext);
156+
}
157+
152158
// See https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.routing.dynamicroutevaluetransformer.transformasync?view=aspnetcore-5.0#Microsoft_AspNetCore_Mvc_Routing_DynamicRouteValueTransformer_TransformAsync_Microsoft_AspNetCore_Http_HttpContext_Microsoft_AspNetCore_Routing_RouteValueDictionary_
153159
// We should apparenlty not be modified these values.
154160
// So we create new ones.
@@ -164,7 +170,7 @@ public override async ValueTask<RouteValueDictionary> TransformAsync(HttpContext
164170
return newValues;
165171
}
166172

167-
private async Task<IPublishedRequest> RouteRequestAsync(HttpContext httpContext, IUmbracoContext umbracoContext)
173+
private async Task<IPublishedRequest> RouteRequestAsync(IUmbracoContext umbracoContext)
168174
{
169175
// ok, process
170176

0 commit comments

Comments
 (0)