diff --git a/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsPolicyBuilder.cs b/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsPolicyBuilder.cs
index 3953296168..145571ab60 100644
--- a/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsPolicyBuilder.cs
+++ b/src/Microsoft.AspNetCore.Cors/Infrastructure/CorsPolicyBuilder.cs
@@ -17,6 +17,7 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure
/// Creates a new instance of the .
///
/// list of origins which can be added.
+ /// for details on normalizing the origin value.
public CorsPolicyBuilder(params string[] origins)
{
WithOrigins(origins);
@@ -36,16 +37,55 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure
///
/// The origins that are allowed.
/// The current policy builder.
+ ///
+ /// This method normalizes the origin value prior to adding it to to match
+ /// the normalization performed by the browser on the value sent in the ORIGIN header.
+ ///
+ /// -
+ /// 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
+ /// collection.
+ ///
+ /// -
+ /// For all other origins, normalization involves performing a culture invariant lower casing of the host name.
+ ///
+ ///
+ ///
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();
+ }
+
///
/// Adds the specified to the policy.
///
@@ -222,4 +262,4 @@ namespace Microsoft.AspNetCore.Cors.Infrastructure
return this;
}
}
-}
\ No newline at end of file
+}
diff --git a/test/Microsoft.AspNetCore.Cors.Test/CorsPolicyBuilderTests.cs b/test/Microsoft.AspNetCore.Cors.Test/CorsPolicyBuilderTests.cs
index c9b3afca84..6aa6cd0cd7 100644
--- a/test/Microsoft.AspNetCore.Cors.Test/CorsPolicyBuilderTests.cs
+++ b/test/Microsoft.AspNetCore.Cors.Test/CorsPolicyBuilderTests.cs
@@ -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);
+ }
}
-}
\ No newline at end of file
+}