Enabling CORS in ASP.NET Core —— 跨站攻击

原文: https://code-maze.com/enabling-cors-in-asp-net-core/

In this post, we are going to talk about enabling CORS in ASP.NET Core Web Applications.

To download the source code for this article, you can visit our Enabling CORS in ASP.NET Core repository.

This post is divided into the following sections:

Before we understand how to enable CORS in ASP.NET Core applications, we need to understand an important concept in the web application security model called the Same-Origin Policy.

What Is CORS?

Let’s learn what the term same-origin actually means first.

About Same-Origin and What it Means

Two URLs are considered to have the same origin if they have the same URL scheme (HTTP or HTTPS), same hostname (domain name), and same port number (the endpoint at which applications talk to each other).

 

https://baike.baidu.com/item/CORS/16411212?fr=aladdin

CORS,全称Cross-Origin Resource Sharing [1]  ,是一种允许当前域(domain)的资源(比如html/js/web service)被其他域(domain)的脚本请求访问的机制,通常由于同域安全策略(the same-origin security policy)浏览器会禁止这种跨域请求

 

 

 

 

What Is the Same-Origin policy?

The Same-origin policy states that a Web browser will only allow communication between two URLs if they belong to the same origin. That is, the client app (https://example.com) cannot communicate with the server app (https://example.net) as they belong to a different origin. This policy exists to isolate potentially malicious scripts from harming other documents on the web.

But there are some cases where Cross-Domain scripting is desired and the idea of the open web makes it compelling.

Cross-Origin Resource Sharing is a mechanism to bypass the Same-Origin policy of a Web browser. Specifically, a server app uses additional HTTP headers to tell a browser that it is fine to load documents (from its origin) in a few selected client apps (of different origin).

 

But how does CORS actually work?

Let’s have a look at the picture below. There is a client app (https://example.com) sending a GET request to a server app (https://example.net) for some resource:

 

 

 

All modern browsers set the Origin header automatically, which indicates the domain of the site is making the request.

If the server allows Cross-origin requests from the Origin (https://example.com), it sets the Access-Control-Allow-Origin header with its value matching the origin header’s value from the request.

In another way, if the server doesn’t include this header, the request fails. The browser shall receive the response data, but this data shall not be accessible to the client.

That’s how a simple CORS request works.

 

 

 

Preflight Requests

Sometimes, instead of a simple GET request, a client may need to send requests like PUT, DELETE, etc. For such requests, the browser sends an additional request (an OPTIONS request) called a Preflight request. This is done just before the actual request to make sure that the original request succeeds. If it does, the browser sends the actual request.

This diagram illustrates such a scenario:

 

 

 

 

 

 

 

 

 

example

 

Creating an Application Without CORS

In the demo, let’s create two projects. The first one is going to be a Web API project, and the second one is going to be a Blazor WebAssembly project.

Let’s create a Web API project that will be our server. Once we create this project, we are going to modify the launchSettings. json file:

 

{
  "profiles": {
    "CorsServerApp": {
      "commandName": "Project",
      "dotnetRunMessages": "true",
      "launchBrowser": false,
      "launchUrl": "weatherforecast",
      "applicationUrl": "https://localhost:5001;http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

  

As you can see, this app will run on the https://localhost:5001 URI.

 

 

Next, let’s create a Client app. We are going to create a new Blazor WebAssembly standalone project. Of course, we have to modify the launchSettings.json file as well:

{
  "profiles": {
    "CorsClientApp": {
      "commandName": "Project",
      "dotnetRunMessages": "true",
      "launchBrowser": true,
      "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
      "applicationUrl": "https://localhost:5011;http://localhost:5010",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

  

Okay. We have both a client and the server app running and they belong to different origins. The server app will run on port 5001 and the client app on port 5011.

 

To make cross-origin requests, we first have to modify the Program.cs file of the client’s app:

public static async Task Main(string[] args)
{
    var builder = WebAssemblyHostBuilder.CreateDefault(args);
    builder.RootComponents.Add<App>("#app");

    builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri("https://localhost:5001") });
        
    await builder.Build().RunAsync();
}

  

Here, we just set up the base address for the HttpClient, which is the URI of our Web API.

Then, we have to open the Pages/FetchData.razor file, and modify the @code part:

@code {
    private WeatherForecast[] forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("/weatherforecast");
    }

    ...
}

Since we already modified the base address of the HttpClient, by adding this change, we want to send our GET request to the https://localhost:5001/weatherforecast URI, which is the exact address of the GET action inside the WeatherForecast controller in the server app.

 

Since we haven’t enabled CORS on the server app, the server app doesn’t set any headers and the browser rejects the response.

So, if we start both applications, navigate to the client app, and open the Fetchdata page, we are going to see the error message:

 

 

 So, we can see that the request is blocked by CORS policy and our client app is not allowed to fetch the data from the required resource.

 

Enabling CORS in ASP.NET Core Middleware

Now that we have seen the Same-Origin policy in action, let’s see how we can enable CORS in ASP.NET Core.

To do that, let’s open the Startup.cs file in the server app and modify it:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }
    private readonly string _policyName = "CorsPolicy";

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCors(opt =>
        {
            opt.AddPolicy(name: _policyName, builder =>
            {
                builder.AllowAnyOrigin()
                    .AllowAnyHeader()
                    .AllowAnyMethod();
            });
        });

        services.AddControllers();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseHttpsRedirection();

        app.UseRouting();
        app.UseCors(_policyName);

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}

  

First, we create a private variable to hold the name of our CORS policy. Then, in the ConfigureServices method,

we call the AddCors method that adds cross-origin resource sharing services to the service collection. This method accepts an Action delegate as a parameter where we can configure the CORS options.

For the CORS options, we call the AddPolicy method, which adds a new policy to the configuration and also accepts an Action delegate as a parameter where we can set up the methods to build our policy.

 

With this policy, we enable access from any origin AllowAnyOrigin, allow any header in a request AllowAnyHeader, and allow any method AllowAnyMethod

 

We can agree that this policy is not the strict one, but it is a basic setup that will remove the CORS issue for our client application.

 

Also in the Configure method, we call the UseCors method and pass the policy name to add the CORS middleware to the application’s pipeline. It is very important that we call this method after the UseRouting method and before the UseAuthorization method.

Now, with these changes in place, we can start both of our apps and navigate to the FetchData page:

 

 

There we go. We have enabled CORS in our application and removed the issue we were facing before.

 

 

Default Policies for CORS in ASP.NET Core

In the case where we don’t want to use multiple named policies but a single default policy, we can replace the AddPolicy method with the AddDefaultPolicy method:

services.AddCors(opt =>
{
    opt.AddDefaultPolicy(builder =>
    {
        builder.AllowAnyOrigin()
            .AllowAnyHeader()
            .AllowAnyMethod();
    });
});

  

As you can see, we are not providing the name for the policy. Additionally, we don’t have to provide the policy name in the UseCors method:

app.UseRouting();
app.UseCors();

app.UseAuthorization();

  

For the rest of the article, we are going to stick with the named CORS policy configuration.

  

Enabling CORS in ASP.NET Core with Attributes

If we only want to allow CORS requests to a selected few methods, instead of enabling CORS at the entire application level, we can also enable CORS at the controller level or at the action level. To be able to do that, we have to use the [EnableCors] attribute:

  • We can use just the [EnableCors] attribute on top of the controller or the action, and it will implement a default CORS policy
  • Or we can use the [EnableCors("Policy name")] attribute, to apply a named CORS policy

By using the named policy with the [EnableCors] attribute, we can apply different policies to different controllers or actions.

So, in our example, if we want, we can add one more named policy for the CORS configuration:

private readonly string _policyName = "CorsPolicy";
private readonly string _anotherPolicy = "AnotherCorsPolicy";

public void ConfigureServices(IServiceCollection services)
{
    services.AddCors(opt =>
    {
        opt.AddPolicy(name: _policyName, builder =>
        {
            builder.AllowAnyOrigin()
                .AllowAnyHeader()
                .AllowAnyMethod();
        });
        opt.AddPolicy(name: _anotherPolicy, builder =>
        {
            builder.AllowAnyOrigin()
                .AllowAnyHeader()
                .AllowAnyMethod();
        });
    });

    services.AddControllers();
}

  

Also, we have to modify the Configure method:

app.UseRouting();
app.UseCors();

  

This time, we are not specifying the policy name since we are using more than one policy in our application.

Finally, to see the [EnableCors] attribute in action, we are going to modify the WeatherForecastController:

[EnableCors("CorsPolicy")]
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
    var rng = new Random();
    return Enumerable.Range(1, 5).Select(index => new WeatherForecast
    {
        Date = DateTime.Now.AddDays(index),
        TemperatureC = rng.Next(-20, 55),
        Summary = Summaries[rng.Next(Summaries.Length)]
    })
    .ToArray();
}

[EnableCors("AnotherCorsPolicy")]
[HttpGet("anotherPolicyExample")]
public IActionResult GetTempAboveLimit()
{
    return Ok("This action is protected with another named CORS policy");
}

  

As you can see for the first action, we are enabling CORS using the old named policy – CorsPolicy. But for the other action, we are using a new policy – AnotherCorsPolicy.

Okay, now we know how to apply different policies while enabling CORS. But it is not a good practice to use two different named policies that apply the same policy options. In this situation, only one policy would be sufficient.

So, let’s see how we can configure different policy options and make sense of using different named policies in our application.

Configuring CORS Policy

We can use various options while adding policies to the CORS configuration. 

Right now, for both policies, we are allowing any origin, any header, and any method. But let’s say, we want to allow only our client application to access the first GET action from the WeatherForecast controller, and some other client (on port 5021) to access the second action:

public void ConfigureServices(IServiceCollection services)
{
    services.AddCors(opt =>
    {
        opt.AddPolicy(name: _policyName, builder =>
        {
            builder.WithOrigins("https://localhost:5011")
                .AllowAnyHeader()
                .AllowAnyMethod();
        });
        opt.AddPolicy(name: _anotherPolicy, builder =>
        {
            builder.WithOrigins("https://localhost:5021")
                .AllowAnyHeader()
                .AllowAnyMethod();
        });
    });

    services.AddControllers();
}

  

This time, we are using the WithOrigins method, which accepts a params string[] origins as a parameter.

This means, if we want to allow access to multiple origins for a single policy, we can just add multiple comma-separated URIs as an argument: WithOrigins("First URI", "Second URI")

 

Configuring CORS for Multiple Subdomains

One more thing. When we want to allow access for a client that has a main domain and multiple subdomains, we don’t have to add all the URIs with subdomains as arguments. What we can do is use the wildcard in the WithOrigins method followed by the SetIsOriginAllowedToAllowWildcardSubdomains method

opt.AddPolicy(name: _policyName, builder =>
{
    builder.WithOrigins("https://*.code-maze.com")
        .SetIsOriginAllowedToAllowWildcardSubdomains();
});

By using the SetIsOriginAllowedToAllowWildcardSubdomains method, we allow origins to match a configured wildcard domain when evaluating if the origin is allowed.

 

 

Enabling CORS for Specific Methods

If we take a look at our controller, we can see that we have only GET actions inside. Well, we can specify that as well in the CORS configuration:

public void ConfigureServices(IServiceCollection services)
{
    services.AddCors(opt =>
    {
        opt.AddPolicy(name: _policyName, builder =>
        {
            builder.WithOrigins("https://localhost:5011")
                .AllowAnyHeader()
                .WithMethods("GET");
        });
        opt.AddPolicy(name: _anotherPolicy, builder =>
        {
            builder.WithOrigins("https://localhost:5021")
                .AllowAnyHeader()
                .WithMethods("GET");
        });
    });

    services.AddControllers();
}

Now, instead of AllowAnyMethod, we are using WithMethods, which accepts params string[] methods as a parameter.

Of course, this means if we want to allow access to multiple methods, we need to add them as multiple comma-separated strings: WithMethods("PUT", "DELETE", "GET").  

 

Enabling CORS for Specific Headers

In the same way that we use the WithMethods method to specify access for single or multiple methods, we can use the WithHeaders method to allow specific headers to be sent in a CORS request:

opt.AddPolicy(name: _anotherPolicy, builder =>
{
    builder.WithOrigins("https://localhost:5021")
        .WithHeaders(HeaderNames.ContentType, HeaderNames.Accept)
        .WithMethods("GET");
});

 

Additionally, when we want to expose some headers, we can use the WithExposedHeaders method.

For example, in our Blazor WebAssembly Pagination with ASP.NET Core Web API article, we create a pagination functionality on the Web API’s side and add the required information inside the X-Pagination header:

 

[HttpGet]
public async Task<IActionResult> Get([FromQuery] ProductParameters productParameters)
{
    var products = await _repo.GetProducts(productParameters);
    Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(products.MetaData));
    return Ok(products);
}

  

But for our client application to be able to access that information inside the header, we have to expose it in the CORS configuration:

opt.AddPolicy(name: _anotherPolicy, builder =>
{
    builder.WithOrigins("https://localhost:5021")
        .WithHeaders(HeaderNames.ContentType, HeaderNames.Accept)
        .WithMethods("GET")
        .WithExposedHeaders("X-Pagination");
});

  

And that’s it for our CORS configuration.

Additional read: You can see the CORS in action with .NET Core and Angular projects by reading the .NET Core tutorial.

How to Disable CORS

Finally, a small note on disabling CORS. Sometimes we need to disable CORS on the action or the controller level.

To do that, we can use the [DisableCors] attribute:

[DisableCors]
[HttpGet]
public async Task<IActionResult> Get([FromQuery] ProductParameters productParameters)
{
    var products = await _repo.GetProducts(productParameters);
    Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(products.MetaData));
    return Ok(products);
}

  

That’s it for now. Let’s recap what we’ve learned.

Conclusion

Think of CORS as a relaxation attempt to the more restrictive Same-Origin policy.

On one side, there is a growing need for security on the web. And on the other, there is a need to integrate web services.

CORS provides rich tools for our need for an open and secure web, and ASP.NET Core allows us to take advantage of CORS in our cross-platform web applications.

 

 

 

for example:

web api:

 

when default:

 

 

 

 

 

 

 

 

 

 

 

插入代码:

app.UseCors(CommonConsts.ServiceConsts.CorsPolicyForAllowAny);
app.UseRouting();

//call UseCors method after the UseRouting method and before the UseAuthorization method.
//https://code-maze.com/enabling-cors-in-asp-net-core/
app.UseCors(CommonConsts.ServiceConsts.CorsPolicyForAllowAny);

app.UseAuthentication();

  

 

 

 

posted @ 2023-03-06 10:51  PanPan003  阅读(61)  评论(0编辑  收藏  举报