Normalize internationalized domain names when adding to CORS
Fixes https://github.com/aspnet/Home/issues/3353
This commit is contained in:
parent
e8caccf009
commit
6f76189846
|
|
@ -17,6 +17,7 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure
|
|||
/// Creates a new instance of the <see cref="CorsPolicyBuilder"/>.
|
||||
/// </summary>
|
||||
/// <param name="origins">list of origins which can be added.</param>
|
||||
/// <remarks> <see cref="WithOrigins(string[])"/> for details on normalizing the origin value.</remarks>
|
||||
public CorsPolicyBuilder(params string[] origins)
|
||||
{
|
||||
WithOrigins(origins);
|
||||
|
|
@ -36,16 +37,55 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure
|
|||
/// </summary>
|
||||
/// <param name="origins">The origins that are allowed.</param>
|
||||
/// <returns>The current policy builder.</returns>
|
||||
/// <remarks>
|
||||
/// This method normalizes the origin value prior to adding it to <see cref="CorsPolicy.Origins"/> to match
|
||||
/// the normalization performed by the browser on the value sent in the <c>ORIGIN</c> header.
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// If the specified origin has an internationalized domain name (IDN), the punycoded value is used. If the origin
|
||||
/// specifies a default port (e.g. 443 for HTTPS or 80 for HTTP), this will be dropped as part of normalization.
|
||||
/// Finally, the scheme and punycoded host name are culture invariant lower cased before being added to the <see cref="CorsPolicy.Origins"/>
|
||||
/// collection.
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// For all other origins, normalization involves performing a culture invariant lower casing of the host name.
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public CorsPolicyBuilder WithOrigins(params string[] origins)
|
||||
{
|
||||
foreach (var origin in origins)
|
||||
{
|
||||
_policy.Origins.Add(origin.ToLowerInvariant());
|
||||
var normalizedOrigin = GetNormalizedOrigin(origin);
|
||||
_policy.Origins.Add(normalizedOrigin);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
internal static string GetNormalizedOrigin(string origin)
|
||||
{
|
||||
if (Uri.TryCreate(origin, UriKind.Absolute, out var uri) &&
|
||||
(uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps) &&
|
||||
!string.Equals(uri.IdnHost, uri.Host, StringComparison.Ordinal))
|
||||
{
|
||||
var builder = new UriBuilder(uri.Scheme.ToLowerInvariant(), uri.IdnHost.ToLowerInvariant());
|
||||
if (!uri.IsDefaultPort)
|
||||
{
|
||||
// Uri does not have a way to differentiate between a port value inferred by default (e.g. Port = 80 for http://www.example.com) and
|
||||
// a default port value that is specified (e.g. Port = 80 for http://www.example.com:80). Although the HTTP or FETCH spec does not say
|
||||
// anything about including the default port as part of the Origin header, at the time of writing, browsers drop "default" port when navigating
|
||||
// and when sending the Origin header. All this goes to say, it appears OK to drop an explicitly specified port,
|
||||
// if it is the default port when working with an IDN host.
|
||||
builder.Port = uri.Port;
|
||||
}
|
||||
|
||||
return builder.Uri.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped);
|
||||
}
|
||||
|
||||
return origin.ToLowerInvariant();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified <paramref name="headers"/> to the policy.
|
||||
/// </summary>
|
||||
|
|
@ -222,4 +262,4 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure
|
|||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
|
@ -299,5 +299,85 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure
|
|||
var corsPolicy = builder.Build();
|
||||
Assert.False(corsPolicy.SupportsCredentials);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Some-String", "some-string")]
|
||||
[InlineData("x:\\Test", "x:\\test")]
|
||||
[InlineData("FTP://Some-url", "ftp://some-url")]
|
||||
public void GetNormalizedOrigin_ReturnsLowerCasedValue_IfStringIsNotHttpOrHttpsUrl(string origin, string expected)
|
||||
{
|
||||
// Act
|
||||
var normalizedOrigin = CorsPolicyBuilder.GetNormalizedOrigin(origin);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, normalizedOrigin);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetNormalizedOrigin_DoesNotAddPort_IfUriDoesNotSpecifyOne()
|
||||
{
|
||||
// Arrange
|
||||
var origin = "http://www.example.com";
|
||||
|
||||
// Act
|
||||
var normalizedOrigin = CorsPolicyBuilder.GetNormalizedOrigin(origin);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(origin, normalizedOrigin);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetNormalizedOrigin_LowerCasesScheme()
|
||||
{
|
||||
// Arrange
|
||||
var origin = "HTTP://www.example.com";
|
||||
|
||||
// Act
|
||||
var normalizedOrigin = CorsPolicyBuilder.GetNormalizedOrigin(origin);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("http://www.example.com", normalizedOrigin);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetNormalizedOrigin_LowerCasesHost()
|
||||
{
|
||||
// Arrange
|
||||
var origin = "http://www.Example.Com";
|
||||
|
||||
// Act
|
||||
var normalizedOrigin = CorsPolicyBuilder.GetNormalizedOrigin(origin);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("http://www.example.com", normalizedOrigin);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("http://www.Example.com:80", "http://www.example.com:80")]
|
||||
[InlineData("https://www.Example.com:8080", "https://www.example.com:8080")]
|
||||
public void GetNormalizedOrigin_PreservesPort_ForNonIdnHosts(string origin, string expected)
|
||||
{
|
||||
// Act
|
||||
var normalizedOrigin = CorsPolicyBuilder.GetNormalizedOrigin(origin);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, normalizedOrigin);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("http://Bücher.example", "http://xn--bcher-kva.example")]
|
||||
[InlineData("http://Bücher.example.com:83", "http://xn--bcher-kva.example.com:83")]
|
||||
[InlineData("https://example.қаз", "https://example.xn--80ao21a")]
|
||||
[InlineData("http://😉.fm", "http://xn--n28h.fm")]
|
||||
// Note that in following case, the default port (443 for HTTPS) is not preserved.
|
||||
[InlineData("https://www.example.இந்தியா:443", "https://www.example.xn--xkc2dl3a5ee0h")]
|
||||
public void GetNormalizedOrigin_ReturnsPunyCodedOrigin(string origin, string expected)
|
||||
{
|
||||
// Act
|
||||
var normalizedOrigin = CorsPolicyBuilder.GetNormalizedOrigin(origin);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, normalizedOrigin);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue