dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.45k stars 10.03k forks source link

Add support for content security policy #6001

Open javiercn opened 7 years ago

javiercn commented 7 years ago

This is a placeholder issue.

Similar to the support that we have for CORS in APIs, we should have support for Content Security Policy to make sites safer by default. Support for CSP would be policy based, similar to the one we offer for CORS.

Usage from middleware

ConfigureServices(IServiceCollection services)
{
    ...
    services.AddCsp();
    ...
}
Configure(IApplicationBuilder app)
{
    ...
    app.UseCsp();
    ...
}

Usage from MVC

ConfigureServices(IServiceCollection services)
{
    ...
    services.AddMvc(); // Add MVC will call AddCsp similar to what we do for CORS today.
    ...
}
[EnableCsp]
public IActionResult Index()
{
    return View();
}

We will provide a default policy that limits content to your domain, defines best practices for HTTPS and will be set to report-only. This behavior can be switched per endpoint so that you can progressively enforce the policy one endpoint at a time.

References

https://en.wikipedia.org/wiki/Content_Security_Policy

https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP

https://www.w3.org/TR/CSP2/

http://caniuse.com/#search=content%20security%20policy

muratg commented 6 years ago

We'll need a design for this guys.

muratg commented 6 years ago

cc @shirhatti

jrestall commented 6 years ago

I created a policy based Content Security Policy library to use on my own sites while waiting for this support in the core libraries. Since content security policy support is now planned for 3.0, it might help you with the design discussion. I also followed your standards on the off chance you could use any of the code for this issue.

Code

Microsoft.AspNetCore.Csp Microsoft.AspNetCore.Mvc.Csp

Samples

CspSample CspSample.Mvc

public void ConfigureServices(IServiceCollection services)
{
    services.AddCsp(options =>
    {
        options.AddPolicy("Policy1", policy => policy
            .AddDefaultSrc(src =>
            {
                src.AllowSchema(CspDirectiveSchemas.Http);
                src.AllowSchema(CspDirectiveSchemas.Https);
                src.AllowSelf();
                src.AllowEval();
                src.AllowHash("sha256-qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng=");
            })
            .AddScriptSrc(src => src.AddNonce())
            .AddStyleSrc(src => src.AddNonce())
            .ReportUri("/csp-reports")
            .ReportTo("csp-reports")
            .AddManifestSrc(src =>
            {
                src.AllowHost("http://*.example.com");
            })
            .ReportOnly()
        );

        options.AddPolicy("Policy2", policy =>
            policy.AddDefaultSrc(src => src.AllowNone().AddNonce())
        );

        options.AddPolicy("BetaUsers", policy =>
            policy.AddDefaultSrc(src => src.AllowHost("beta-testers.example.org"))
        );
    });

    services.AddMvc()
        .AddCspReportMediaType();

    services.AddMvcCore().AddCsp();

    services.AddScoped<IConfigureOptions<CspOptions>, TrialUserSrc>();
}

public class TrialUserSrc :  IConfigureOptions<CspOptions>
{
    public void Configure (CspOptions options)
    {
        if (context.User.HasClaim(c => c.Type == ClaimTypes.TrialUser))
        {
            var currentPolicy = options.GetPolicy("Policy1");
        currentPolicy.Append(policy => 
                policy.AddDefaultSrc(src => src.AllowHost("trial.company.com"))
            );
        }
    }
}

Configuration with MVC Attributes

[DisableCsp] public IActionResult Disabled() { ... }


### Default Content-Security-Policy
This is the default policy I'm using:
```csharp
    public class CspOptions
    {
        // Default Content-Security-Policy:
        //     default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:;
        //     object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'
        private static readonly ContentSecurityPolicy DefaultContentSecurityPolicy

Tag Helpers

CspMetaTagHelper - Adds the content security policies as meta tags to the page.

<meta http-equiv="Content-Security-Policy" />

CspNonceTagHelper - Adds a nonce to script or style tags and automatically ensures a nonce is returned in the HTTP header.

CspGenerateHashTagHelper - Automatically generates hashes for inline scripts and styles and ensures the hashes are in the HTTP header.

<style asp-add-nonce="true" asp-generate-hash="true">
    p {
        color: #0000ff;
    }
</style>
<script asp-add-nonce="true" asp-generate-hash="true" asp-generate-hash-algorithms="HashAlgorithms.SHA256 | HashAlgorithms.SHA512">
    console.log("I'm a script that will be hashed dynamically.");
</script>

CspPluginTypeTagHelper - Automatically adds the used plugin types to the content security policy.

<embed asp-type="application/testplugintype2" />

CspFallBackTagHelper - Generates CSP hashes for the inline scripts that are generated when using asp-fallback-href or asp-fallback-src.

CspSubresourceIntegrityTagHelper - Automatically generates SRI hashes for the remote scripts using a local fallback script.

<link href="https://ajax.aspnetcdn.com/ajax/bootstrap/4.0.0/css/bootstrap.min.css" rel="stylesheet"
      asp-fallback-href="~/lib/bootstrap/css/bootstrap.min.css"
      asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute"
      asp-subresource-integrity="true"
      asp-subresource-integrity-algorithms="HashAlgorithms.SHA256 | HashAlgorithms.SHA384 | HashAlgorithms.SHA512" />

CSP Report Support

/// <summary>
/// Adds the 'application/csp-report' media type to the JSON input formatter so that csp reports can be received.
/// </summary>
/// <param name="builder">The <see cref="IServiceCollection" /> to add services to.</param>
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
public static IMvcBuilder AddCspReportMediaType(this IMvcBuilder builder)

cc @sebastienros - When this feature lands in 3.0 I'll implement CSP for Orchard Core :tada:

javiercn commented 6 years ago

@jrestall Your approach is pretty solid. It's pretty much what I had in mind. I have some ideas on how to better integrate this with Razor, but the foundation is solid.

The way I see this, there will be a core library (similar to what we have for CORS) and then some MVC specific glue/enhancements/opinions.

In particular, I think it might be better to specify the policy as a Razor directive as its very resource specific. In many actions you don't have the ability to know what's going to be rendered. By doing it as a razor directive it can be something that you put on a _viewStart, _viewImports (not really sure which one of them yet) or simply at the start of your page or view.

That way the policy lives close to the resources, regarding hashes and things like that I would have to think more about this. One thing that I'm concern is the size of the policy on the header growing to a significant size, so I'm more inclined to use/recommend a different approach where you hoist the inline-scripts into js files for most cases.

I think that you are on point with the need to append to an initial policy and to completely override it in some cases.

Having a default policy is also great, but the default for the system should be to simply report so that you can start changing the code base to enforce the policy.

shirhatti commented 5 years ago

@mkArtakMSFT @javiercn Is this still happening in 3.0?

ulrichb commented 5 years ago

Please don't forget about the validation summary (inline style-element) mentioned in https://github.com/aspnet/AspNetCore/issues/4817#issuecomment-308058241.

https://github.com/aspnet/AspNetCore/blob/bcead68f0cebf53c1c8fe4c1015cc6961f5a757c/src/Mvc/Mvc.ViewFeatures/src/DefaultHtmlGenerator.cs#L24

chrisdpratt commented 5 years ago

Interesting that no one has mentioned NWebSec. This library started life adding much needed security enhancements to ASP.NET MVC, and now support ASP.NET Core as well. There's already a pretty complete CSP implementation here, so at the very least, it might be worth referencing.

https://docs.nwebsec.com/en/latest/nwebsec/libraries.html

joeaudette commented 5 years ago

I'm looking forward to this being part of the aspnet core stack as well. I've experimented a bit with NWebSec but it seems it has not been updated in over a year, issues go unanswered and even their SSL cert for their website expired in Feb 2019 and they have not done anything about it. Seems like an abandoned project to me. https://www.nwebsec.com/

nbarbettini commented 4 years ago

NWebSec is solid, but as @joeaudette said it's under-maintained right now. I would love to see something like @jrestall's approach built in.

NWebSec puts everything into Configure, but it would be easier to compose with things like the Options pattern if configuration happened in ConfigureServices like in James' example.

aaronshim commented 4 years ago

Hi folks, My colleague @salcho has been in contact with some of you for adding framework-level support of CSP for ASP.NET. We've created #24548 of our proposed changes, so we will welcome any feedback! Thank you!

V4A001 commented 4 years ago

Is this issue included: validation summary emitting style="display:none". [Report Only] Refused to apply inline style because it violates the following Content

`

  • `
Ponant commented 3 years ago

@javiercn , I do not see a real benefit of having a csp as a directive. Rather, I think it is better to make the templates more Csp compliant for those who already are implementing Csp (with or without a library) and who may find themselves in using unsafe keywords in the Csp directive set:

One example is the style in the default generator as mentioned above https://github.com/dotnet/aspnetcore/blob/ff51fd7105a9003841215f1f3b0b8fc9e2998a67/src/Mvc/Mvc.ViewFeatures/src/DefaultHtmlGenerator.cs#L28

Another one is the injection of javascript during development, see @damienbod here https://github.com/dotnet/aspnetcore/issues/34428

Regarding the library, from my own experience with Csp at least, I see most benefit in having enable/disable attributes, filters and a global Csp/ReportOnly. For Spa's, I don't know yet, so probably meta tags with hashes (?).

misterspeedy commented 3 years ago

Please don't forget about the validation summary (inline style-element) mentioned in #4817 (comment).

Narrator: They did forget about it.

Ponant commented 3 years ago

I created a library for CSP because we needed it in production apps and existing libraries including NWebsec did not fully fulfill our conditions. It is based on a pragmatic and performance-oriented design philosophy which I believe is worth sharing after a brief exchange with @damienbod here https://github.com/dotnet/aspnetcore/issues/34428. This lead us to introduce the concept of a CSP Policy Group. This class encompasses the Content-Security-Policy and Content-Security-Policy-Report-Only headers in one entity, because they can and should be used in tandem.

So I go along by copy pasting some text, and you can get extensive details here https://github.com/Ponant/Galebra.Security/tree/master/src/Galebra.Security.Headers.Csp , in particular the design section, together with a bunch of references. Critics and/or questions are always welcome.

Get Started

All terminology is explained after. The library does not use EndPoint routing, so you can invoke the UseContentSecurityPolicy middleware before or after UseRouting.

app.UseContentSecurityPolicy();

It can be placed before UseStaticFiles if you need CSP headers to be delivered with peculiar content such as SVG.

You configure CSP via appsettings.json or via an Action in Program.cs.

When using appsetting.json:

using Galebra.Security.Headers.Csp;

builder.Services.AddContentSecurityPolicy(builder.Configuration.GetSection("Csp"));

There are two services registered, a Singleton, essentially holding the configuration, and a Scoped service for nonce generation.

In the following, three policy groups are registered:

  "Csp": {
    "IsDisabled": false,//default: Will apply the default policy everywhere until overriden by attributes or filters
    "PolicyGroups": {
      "PolicyGroup1": {
        "Csp": {
          "Fixed": "default-src 'none' 'sha256-RFWPLDbv2BY+rCkDzsE+0fr8ylGr2R2faWMhq4lfEQc=';script-src 'self'"
        },
        "IsDefault": false,
        "NumberOfNonceBytes": 16//default
      },
      "PolicyGroup2": {
        "Csp": {
          "Fixed": "default-src 'self';base-uri 'self';form-action 'self';object-src;frame-ancestors;connect-src ws://localhost:65412",
          "Nonceable": [
            "style-src 'self'"
          ]
        },
        "CspReportOnly": {
          "Fixed": "default-src;form-action 'self';base-uri;object-src;frame-ancestors;sandbox",
          "Nonceable": [
            "style-src",
            "script-src"
          ]
        },
        "IsDefault": true,//default
        "NumberOfNonceBytes": 8
      },
      "PolicyGroup3": {
        "Csp": {
          "Nonceable": [
            "style-src"
          ]
        },
        "IsDefault": false,
        "NumberOfNonceBytes": 3
      }
    }
  },

The first policy group does not require nonces (hence fixed, see below) and only requires the Content-Security-Policy header to be set. The second policy group configures the two headers, CSP and CSP-Report-Only, and requires nonces for each of these headers. This policy is the default policy, IsDefault=true. The IsDisabled property in Line 1 is set to false (default), which means that the default policy named PolicyGroup2 will be applied globally unless overridden by attributes or filters. The third policy uses only nonces, for styles. The default value for nonce generation is 16 bytes. We used connect-src ws://localhost:65412 in this example to allow /_framework/aspnetcore-browser-refresh.js to work properly. Also, we disabled CSS Hot Reload in Visual Studio, see https://github.com/dotnet/aspnetcore/issues/36085, to avoid a weak CSP configuration just for development.

Alternatively, you can configure everything in code:

builder.Services.AddContentSecurityPolicy(c =>
{
    c.IsDisabled = false;
    c.Add("Policy1", g =>
    {
        g.Csp.Fixed = "default-src 'self';connect-src ws://localhost:65412";
        g.Csp.Nonceable.Add("style-src 'self'");
        g.CspReportOnly.Nonceable.Add("script-src");
        g.IsDefault = true;
        g.NumberOfNonceBytes = 32;
    });
    c.Add("Policy2", g =>
    {
        g.Csp.Fixed = "default-src 'self';connect-src ws://localhost:65412";
        g.CspReportOnly.Fixed="default-src";
        g.IsDefault = false;
    });
});

Add nonces to the body by importing the TagHelpers

@addTagHelper *, Galebra.Security.Headers.Csp

And add to styles, scripts or link tags the TagHelper:

nonce-add=true

For example, for PolicyGroup3, that restricts you to use only nonced styles, you would allow loading bootstrap like so:

    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" nonce-add=true/>

As you may agree, this logical structure actually is powerful.

Design Philosophy and introduction

The following design principles, detailed below, have been followed:

  • CSP configuration is well suited in appsettings.json because you need to store long strings
  • Configuration via Action should also be possible in Program.cs
  • Support multiple policies to use in different parts of the website
  • Enable and Disable attributes in pages or actions or controllers
  • Enable and Disable Filters for folders in razor pages
  • Developper provides a default, global policy, and can override
  • Possibility to apply the default policy everywhere, and override for parts of the website, or have no policy applied globally and set them on specific parts only
  • Support CSP and a CSP-Report-Only headers simultaneously, each with a different set of policy directives
  • Optimize nonce generation for script, style, and link tags
  • Use the latest recommendations for random number generations
  • Optimize string generation for common CSP string lengths
  • Add CSP to headers by using the key already existing in HttpContext to avoid memory allocation
  • Each policy is, at the end, a long string. This string can be either static (fixed) or generated newly on each request if nonces are required.

Consider the following arguments. CSP is a set of two headers, whose names can be:

  • Content-Security-Policy
  • Content-Security-Policy-Report-Only

Each policy in those headers is defined by an ordered set of directives. Each directive is a name/value pair; name is non-empty, but value may be empty. For example, script-src is a directive's name and the value can be, for instance, 'self' or a url or a set of these. Another directive can be default-src 'none', where default-src is another directive name. The directives forming a policy are separated by semicolons. For this example, the Content Security Policy would be the header:

Content-Security-Policy: default-src 'none';script-src 'self'

which is equivalent to the shorter version

Content-Security-Policy: default-src;script-src 'self'

In theory, you can have multiple CSP policies for the same header name in the same response header; however, we believe this should not be recommended because it makes your policy more convoluted and rather highlights a design flaw, notwithstanding that you send more bytes to the user's browser. Consequently, the library outputs only one CSP header for a given header name. In practice, we assign the value for the header key (name) rather than adding a new item in the dictionary. This also has the consequence of efficient write.

So, with this library, you cannot have the following:

Content-Security-Policy: Policy1
Content-Security-Policy: Policy2

The CspPolicyGroup class

However, you can have both a CSP and a CSP-Report-Only policy, and usually this is recommended. Some libraries do not support this configuration. This library does. For example you can do something like this:

Content-Security-Policy: default-src 'self'
Content-Security-Policy-Report-Only: default-src;style-src 'self'

In this scenario, the website would run without issues if, loosely speaking, all styles and scripts are loaded from the server ('self'), but the browser will report to you what would break if you disable scripts ('none') and other fetch directives, except for styles which use 'self'. Thus, this allows you to enforce a policy and at the same time fine-tune another one which ultimately will become the enforcing policy when you are ready.

Allowing for both CSP and CSP-Report-Only headers to coexist introduces the CspPolicyGroup class. It is this group class that you use to configure your policies. This class contains two properties, Csp and CspReportOnly, each of which is a CspPolicy class. You can leave one of these properties empty; for example if you want to have only browser reports, you would build only the property CspReportOnly.

The CspPolicy class and Nonce TagHelper

The usual route to library design is to use the so-called fluent-api. This gives elegant code, but with CSP this is unnecessary complication and makes your Program.cs (or Startup.cs) rather long. In addition, CSP configuration ultimately boils down to outputting one or two strings in the headers, plus the possibility of nonces in those headers and pages. A developer will ultimately look at the CSP output in the browser's tools to see if the formatting is as expected and nonces properly generated and positioned. There is already a lot of resources and services in the pipeline, so we decided to keep it simple and focus on performance.

This led us to the following observation. A CSP header value is always divided into two groups. The first group is a fixed, possibly empty, string, i.e. a string that does not change upon requests on a given page, and another string that is dynamically generated upon each request to include one or more nonces. Therefore the CspPolicy class has two public properties, a string Fixed and a list of strings called Nonceable.

For example, the policy:

default-src;style-src 'self'

would be set as a CspPolicy.Fixed string because a nonce is not required. In appsettings.json, this would be:

"Fixed": "default-src;style-src 'self'",

A sha256-myshacode, if needed, would be included in the Fixed string.

If, now, you want to have this policy, but in addition produce a nonce for styles, then you would need to split your string and populate the CspPolicy.Nonceable list. In appsettings.json, the split would be like this:

"Fixed": "default-src",
"Nonceable": [
"style-src 'self'"
]

The style-src is here short, but generally is longer to include for example urls from a Bootstrap CDN. The most important is to identify the directive's name, here style-src.

To use the nonce on the style in this example, you would invoke the Tag Helper nonce-add, e.g.

<style nonce-add=true>
.myclass{
    background:lightgreen;
}
</style>
<h4 class="myclass">I am lightgreen, thank you nonce!</h4>

When you use a nonce, common libraries are confined to the script and style tags, but link tags are also possible. Nonce generation defaults to the spec-recommended 16 bytes, which gives 256^16=2^128 possibilities, or a 22 long web-encoded base64 string. You can override this with the property CspPolicyGroup.NumberOfNonceBytes, e.g. in appsettings.json, and the nonce will apply to the entire group.

"NumberOfNonceBytes": 8

Multiple Policies, Attributes, Filters and default CspPolicyGroup

When you implement CSP on a website, often you need several CspPolicyGroup objects depending on the page where the user lands. For example, you would have a global CSP policy on all pages, but when processing a payment on a page or Razor Pages folder, or a Controller, you will want another CSP policy (for example to accept connections to a payment API such as PayPal or Stripe). This library allows you to configure many policies and invoke them when needed, through attributes and filters.

When you use the library, unless you override the default described below, the default CspPolicyGroup is applied to all pages as soon as you inject the Middleware in the pipeline.

You can apply a given policy to an entire Folder in Razor Pages, e.g. in the Movies folder:

//Apply on specific folders
builder.Services.AddRazorPages(options =>
{
    options.Conventions.AddFolderApplicationModelConvention(
        "/Movies",
                model => model.Filters.Add(new EnableCspPageFilter { PolicyGroupName = "PolicyGroup1" }));
});

You can also disable CSP in a folder:

 model => model.Filters.Add(new DisableCspPageFilter { EnforceMode = false }));

Where EnforceMode is true by default and is discussed below.

In areas, you could do something like this:

    options.Conventions.AddAreaFolderApplicationModelConvention("Identity", "/Account",
     model => model.Filters.Add(new EnableCspPageFilter { PolicyGroupName = "PolicyGroup1" }));

    options.Conventions.AddAreaPageApplicationModelConvention("Identity", "/Account/Manage/ChangePassword",
        model => model.Filters.Add(new EnableCspPageFilter { PolicyGroupName = "PolicyGroup3" }));

In a Razor Page or Action or Controller, you can override the default CspPolicyGroup with an attribute:

[EnableCsp(PolicyGroupName="PolicyGroup1")]
[DisableCsp]

The [DisableCsp] always wins unless you set the init property DisableCsp.EnforceMode to false. The enforcement rule is useful in a scenario where an entire folder or a controller needs to have CSP disabled on all routes except for those where the enable attribute is present. For example, in the following we use the DisableCspPageFilter to disable CSP in the Movies folder:

builder.Services.AddRazorPages(options =>
{
    options.Conventions.AddFolderApplicationModelConvention(
        "/Movies",
    model => model.Filters.Add(new DisableCspPageFilter { EnforceMode = false }));
});

Because we have set EnforceMode = false, we can set CSP on a page inside the Movies folder with an attribute, e.g. in Movies/index [EnableCsp(PolicyGroupName = "PolicyGroup3")]. The same applies for a controller; you would disable CSP with EnforceMode=false and use the EnableCsp attribute on an action:

[DisableCsp(EnforceMode = false)]
public class BooksController : Controller
{
    public ActionResult Index()
    {
        //CSP is disabled here
        return View();
    }

    // GET: BooksController/Details/5
    [EnableCsp(PolicyGroupName ="PolicyGroup3")]
    public ActionResult Details(int id)
    {
        //CSP works here owing to EnforceMode=false
        return View();
    }
}

IsDisabled global boolean

By default, the library applies the default CspPolicyGroup to all delivered pages until overwritten by attributes or filters. You can override this global behaviour in Program.cs or in appsettings.json where you can set at the top level "IsDisabled": true,. This will result in CSP not being applied globally, at all, until you invoke it via attributes or filters.

niemyjski commented 3 months ago

This would be really nice to be built in. Two other libraries support this as well https://github.com/andrewlock/NetEscapades.AspNetCore.SecurityHeaders and https://github.com/juunas11/aspnetcore-security-headers