Skip to content
This repository has been archived by the owner on Nov 22, 2018. It is now read-only.

Commit

Permalink
Normalize internationalized domain names when adding to CORS
Browse files Browse the repository at this point in the history
  • Loading branch information
pranavkm committed Oct 5, 2018
1 parent e8caccf commit 6f76189
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 4 deletions.
44 changes: 42 additions & 2 deletions src/Microsoft.AspNetCore.Cors/Infrastructure/CorsPolicyBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public class CorsPolicyBuilder
/// 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);
Expand All @@ -36,16 +37,55 @@ public CorsPolicyBuilder(CorsPolicy policy)
/// </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>
Expand Down Expand Up @@ -222,4 +262,4 @@ private CorsPolicyBuilder Combine(CorsPolicy policy)
return this;
}
}
}
}
84 changes: 82 additions & 2 deletions test/Microsoft.AspNetCore.Cors.Test/CorsPolicyBuilderTests.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -299,5 +299,85 @@ public void DisallowCredential_SetsSupportsCredentials_ToFalse()
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);
}
}
}
}

0 comments on commit 6f76189

Please sign in to comment.