Browse Source

Merge pull request #16496 from abpframework/salihozkara/SecurityHeader

Script nonce support for CSP
pull/16699/head
Halil İbrahim Kalkan 3 years ago
committed by GitHub
parent
commit
3cf24faa08
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 20
      docs/en/UI/AspNetCore/Security-Headers.md
  2. 5
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperScriptService.cs
  3. 10
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperStyleService.cs
  4. 22
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/ScriptNonceTagHelper.cs
  5. 3
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalScriptContributor.cs
  6. 6
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/dom-event-handlers.js
  7. 1
      framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/AbpAspNetCoreConsts.cs
  8. 23
      framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Security/AbpSecurityHeaderNonceHelper.cs
  9. 103
      framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Security/AbpSecurityHeadersMiddleware.cs
  10. 14
      framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Security/AbpSecurityHeadersOptions.cs
  11. 7
      npm/packs/jquery/src/abp.jquery.js

20
docs/en/UI/AspNetCore/Security-Headers.md

@ -19,13 +19,31 @@ ABP Framework allows you to add frequently used security headers into your appli
Configure<AbpSecurityHeadersOptions>(options =>
{
options.UseContentSecurityPolicyHeader = true; //false by default
options.ContentSecurityPolicyValue = "object-src 'none'; form-action 'self'; frame-ancestors 'none'";
options.ContentSecurityPolicyValues["object-src"] = new string[] { "'none'" };
options.ContentSecurityPolicyValues["form-action"] = new string[] { "'self'" };
options.ContentSecurityPolicyValues["frame-ancestors"] = new string[] { "'self'" };
options.ContentSecurityPolicyValues["script-src"] = new string[] { "'self'", "'unsafe-inline'", "'unsafe-eval'" };
//adding script-src nonce
options.UseContentSecurityPolicyScriptNonce = true; //false by default
//ignore script nonce source for these paths
options.IgnoredScriptNoncePaths.Add("/my-page");
//ignore script nonce by Elsa Workflows and other selectors
options.IgnoredScriptNonceSelectors.Add(context =>
{
var endpoint = context.GetEndpoint();
return Task.FromResult(endpoint?.Metadata.GetMetadata<PageRouteMetadata>()?.RouteTemplate == "/{YOURHOSTPAGE}");
});
//adding additional security headers
options.Headers["Referrer-Policy"] = "no-referrer";
});
```
> Using the script nonce feature will automatically add the nonce value to your script tags. There is no need to add it manually. However, if you still need to add it manually, you can use 'Html.GetScriptNonce()' to add the nonce value or 'Html.GetScriptNonceAttribute()' to add the nonce attribute value.
> If the header is the same, the additional security headers you defined take precedence over the default security headers. In other words, it overrides the default security headers' values.
## Security Headers Middleware

5
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperScriptService.cs

@ -49,6 +49,9 @@ public class AbpTagHelperScriptService : AbpTagHelperResourceService
var deferText = (defer || Options.DeferScriptsByDefault || Options.DeferScripts.Any(x => file.StartsWith(x, StringComparison.OrdinalIgnoreCase)))
? "defer"
: string.Empty;
output.Content.AppendHtml($"<script {deferText} src=\"{viewContext.GetUrlHelper().Content(file.EnsureStartsWith('~'))}\"></script>{Environment.NewLine}");
var nonceText = (viewContext.HttpContext.Items.TryGetValue(AbpAspNetCoreConsts.ScriptNonceKey, out var nonce) && nonce is string nonceString && !string.IsNullOrEmpty(nonceString))
? $"nonce=\"{nonceString}\""
: string.Empty;
output.Content.AppendHtml($"<script {deferText} {nonceText} src=\"{viewContext.GetUrlHelper().Content(file.EnsureStartsWith('~'))}\"></script>{Environment.NewLine}");
}
}

10
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperStyleService.cs

@ -8,19 +8,23 @@ using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Volo.Abp.AspNetCore.Security;
namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling.TagHelpers;
public class AbpTagHelperStyleService : AbpTagHelperResourceService
{
protected AbpSecurityHeadersOptions SecurityHeadersOptions;
public AbpTagHelperStyleService(
IBundleManager bundleManager,
IOptions<AbpBundlingOptions> options,
IWebHostEnvironment hostingEnvironment) : base(
IWebHostEnvironment hostingEnvironment,
IOptions<AbpSecurityHeadersOptions> securityHeadersOptions) : base(
bundleManager,
options,
hostingEnvironment)
{
SecurityHeadersOptions = securityHeadersOptions.Value;
}
protected override void CreateBundle(string bundleName, List<BundleTagHelperItem> bundleItems)
@ -48,7 +52,9 @@ public class AbpTagHelperStyleService : AbpTagHelperResourceService
if (preload || Options.PreloadStylesByDefault || Options.PreloadStyles.Any(x => file.StartsWith(x, StringComparison.OrdinalIgnoreCase)))
{
output.Content.AppendHtml($"<link rel=\"preload\" href=\"{viewContext.GetUrlHelper().Content(file.EnsureStartsWith('~'))}\" as=\"style\" onload=\"this.rel='stylesheet'\" />{Environment.NewLine}");
output.Content.AppendHtml(SecurityHeadersOptions.UseContentSecurityPolicyScriptNonce
? $"<link rel=\"preload\" href=\"{viewContext.GetUrlHelper().Content(file.EnsureStartsWith('~'))}\" as=\"style\" abp-csp-style />{Environment.NewLine}"
: $"<link rel=\"preload\" href=\"{viewContext.GetUrlHelper().Content(file.EnsureStartsWith('~'))}\" as=\"style\" onload=\"this.rel='stylesheet'\" />{Environment.NewLine}");
}
else
{

22
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/ScriptNonceTagHelper.cs

@ -0,0 +1,22 @@
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers;
namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling.TagHelpers;
[HtmlTargetElement("script")]
[HtmlTargetElement("body")]
public class ScriptNonceTagHelper : AbpTagHelper
{
[HtmlAttributeNotBound]
[ViewContext]
public ViewContext ViewContext { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (ViewContext.HttpContext.Items.TryGetValue(AbpAspNetCoreConsts.ScriptNonceKey, out var nonce) && nonce is string nonceString && !string.IsNullOrEmpty(nonceString))
{
output.Attributes.Add("nonce", nonceString);
}
}
}

3
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalScriptContributor.cs

@ -35,6 +35,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Bundling;
)]
public class SharedThemeGlobalScriptContributor : BundleContributor
{
public override void ConfigureBundle(BundleConfigurationContext context)
{
context.Files.AddRange(new[]
@ -48,6 +49,6 @@ public class SharedThemeGlobalScriptContributor : BundleContributor
"/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js",
"/libs/abp/aspnetcore-mvc-ui-theme-shared/sweetalert2/abp-sweetalert2.js",
"/libs/abp/aspnetcore-mvc-ui-theme-shared/toastr/abp-toastr.js"
});
});
}
}

6
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/dom-event-handlers.js

@ -753,6 +753,10 @@
});
}
abp.dom.initializers.initializeAbpCspStyles = function ($abpCspStyles){
$abpCspStyles.attr("rel", "stylesheet");
}
abp.dom.onNodeAdded(function (args) {
abp.dom.initializers.initializeToolTips(args.$el.findWithSelf('[data-toggle="tooltip"]'));
abp.dom.initializers.initializePopovers(args.$el.findWithSelf('[data-toggle="popover"]'));
@ -760,6 +764,7 @@
abp.dom.initializers.initializeForms(args.$el.findWithSelf('form'), true);
abp.dom.initializers.initializeScript(args.$el);
abp.dom.initializers.initializeAutocompleteSelects(args.$el.findWithSelf('.auto-complete-select'));
abp.dom.initializers.initializeAbpCspStyles($("link[abp-csp-style]"));
abp.dom.initializers.initializeDateRangePickers(args.$el);
});
@ -783,6 +788,7 @@
abp.dom.initializers.initializeForms($('form'));
abp.dom.initializers.initializeAutocompleteSelects($('.auto-complete-select'));
$('[data-auto-focus="true"]').first().findWithSelf('input,select').focus();
abp.dom.initializers.initializeAbpCspStyles($("link[abp-csp-style]"));
});
})(jQuery);

1
framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/AbpAspNetCoreConsts.cs

@ -4,4 +4,5 @@ public static class AbpAspNetCoreConsts
{
public const string DefaultApiPrefix = "api";
public const string DefaultIntegrationServiceApiPrefix = "integration-api";
public const string ScriptNonceKey = "ScriptNonce";
}

23
framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Security/AbpSecurityHeaderNonceHelper.cs

@ -0,0 +1,23 @@
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace Volo.Abp.AspNetCore.Security;
public static class AbpSecurityHeaderNonceHelper
{
public static string GetScriptNonce(this IHtmlHelper htmlHelper)
{
if (htmlHelper.ViewContext.HttpContext.Items.TryGetValue(AbpAspNetCoreConsts.ScriptNonceKey, out var nonce) && nonce is string nonceString && !string.IsNullOrEmpty(nonceString))
{
return nonceString;
}
return string.Empty;
}
public static IHtmlContent GetScriptNonceAttribute(this IHtmlHelper htmlHelper)
{
var nonce = htmlHelper.GetScriptNonce();
return nonce == string.Empty ? HtmlString.Empty : new HtmlString($"nonce=\"{nonce}\"");
}
}

103
framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Security/AbpSecurityHeadersMiddleware.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
@ -11,6 +12,8 @@ namespace Volo.Abp.AspNetCore.Security;
public class AbpSecurityHeadersMiddleware : IMiddleware, ITransientDependency
{
public IOptions<AbpSecurityHeadersOptions> Options { get; set; }
protected const string ScriptSrcKey = "script-src";
protected const string DefaultValue = "object-src 'none'; form-action 'self'; frame-ancestors 'none'";
public AbpSecurityHeadersMiddleware(IOptions<AbpSecurityHeadersOptions> options)
{
@ -28,20 +31,104 @@ public class AbpSecurityHeadersMiddleware : IMiddleware, ITransientDependency
/*The X-Frame-Options HTTP response header can be used to indicate whether or not a browser should be allowed to render a page in a <frame>, <iframe> or <object>. SAMEORIGIN makes it being displayed in a frame on the same origin as the page itself. The spec leaves it up to browser vendors to decide whether this option applies to the top level, the parent, or the whole chain*/
AddHeader(context, "X-Frame-Options", "SAMEORIGIN");
if (Options.Value.UseContentSecurityPolicyHeader)
var requestAcceptTypeHtml = context.Request.Headers["Accept"].Any(x =>
x.Contains("text/html") || x.Contains("*/*") || x.Contains("application/xhtml+xml"));
if (!requestAcceptTypeHtml
|| !Options.Value.UseContentSecurityPolicyHeader
|| await AlwaysIgnoreContentTypes(context)
|| context.GetEndpoint() == null
|| Options.Value.IgnoredScriptNoncePaths.Any(x => context.Request.Path.StartsWithSegments(x.EnsureStartsWith('/'))))
{
AddOtherHeaders(context);
await next.Invoke(context);
return;
}
if (Options.Value.UseContentSecurityPolicyScriptNonce)
{
AddHeader(context, "Content-Security-Policy",
Options.Value.ContentSecurityPolicyValue.IsNullOrEmpty()
? "object-src 'none'; form-action 'self'; frame-ancestors 'none'"
: Options.Value.ContentSecurityPolicyValue);
var randomValue = Guid.NewGuid().ToString("N");
context.Items.Add(AbpAspNetCoreConsts.ScriptNonceKey, randomValue);
}
context.Response.OnStarting(() =>
{
if (context.Response.Headers.ContainsKey("Content-Security-Policy"))
{
return Task.CompletedTask;
}
if (context.Response.ContentType?.StartsWith("text/html") != true)
{
return Task.CompletedTask;
}
if (context.Response.StatusCode is < 200 or > 299)
{
return Task.CompletedTask;
}
AddHeader(context, "Content-Security-Policy", BuildContentSecurityPolicyValue(context));
return Task.CompletedTask;
});
AddOtherHeaders(context);
await next.Invoke(context);
}
private async Task<bool> AlwaysIgnoreContentTypes(HttpContext context)
{
foreach (var selector in Options.Value.IgnoredScriptNonceSelectors)
{
if(await selector(context))
{
return true;
}
}
return false;
}
private void AddOtherHeaders(HttpContext context)
{
foreach (var (key, value) in Options.Value.Headers)
{
AddHeader(context, key, value, true);
}
}
await next.Invoke(context);
protected virtual string BuildContentSecurityPolicyValue(HttpContext context)
{
if (!(Options.Value.UseContentSecurityPolicyScriptNonce &&
context.Items.TryGetValue(AbpAspNetCoreConsts.ScriptNonceKey, out var nonce) &&
nonce is string nonceValue && !string.IsNullOrEmpty(nonceValue)))
{
return ContentSecurityPolicyValuesToCSPString();
}
var scriptSrcValue = "";
if (Options.Value.ContentSecurityPolicyValues.TryGetValue(ScriptSrcKey, out var scriptSrc))
{
scriptSrcValue = string.Join(" ", scriptSrc);
}
scriptSrcValue += $" 'nonce-{nonceValue}'";
return ContentSecurityPolicyValuesToCSPString(true) + $"; {ScriptSrcKey} {scriptSrcValue}";
}
protected virtual string ContentSecurityPolicyValuesToCSPString(bool ignoreScriptSrc = false)
{
if (Options.Value.ContentSecurityPolicyValues.Any())
{
return string.Join("; ",
Options.Value.ContentSecurityPolicyValues.WhereIf(ignoreScriptSrc, x => x.Key != ScriptSrcKey)
.Select(x => $"{x.Key} {string.Join(" ", x.Value)}"));
}
return DefaultValue;
}
protected virtual void AddHeader(HttpContext context, string key, string value, bool overrideIfExists = false)
@ -51,7 +138,7 @@ public class AbpSecurityHeadersMiddleware : IMiddleware, ITransientDependency
context.Response.Headers[key] = value;
return;
}
context.Response.Headers.AddIfNotContains(new KeyValuePair<string, StringValues>(key, value));
}
}
}

14
framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Security/AbpSecurityHeadersOptions.cs

@ -1,17 +1,29 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace Volo.Abp.AspNetCore.Security;
public class AbpSecurityHeadersOptions
{
public bool UseContentSecurityPolicyHeader { get; set; }
public bool UseContentSecurityPolicyScriptNonce { get; set; }
public string ContentSecurityPolicyValue { get; set; }
public Dictionary<string, IEnumerable<string>> ContentSecurityPolicyValues { get; }
public Dictionary<string, string> Headers { get; }
public List<Func<HttpContext, Task<bool>>> IgnoredScriptNonceSelectors { get; }
public List<string> IgnoredScriptNoncePaths { get; }
public AbpSecurityHeadersOptions()
{
Headers = new Dictionary<string, string>();
ContentSecurityPolicyValues = new Dictionary<string, IEnumerable<string>>();
IgnoredScriptNonceSelectors = new List<Func<HttpContext, Task<bool>>>();
IgnoredScriptNoncePaths = new List<string>();
}
}

7
npm/packs/jquery/src/abp.jquery.js

@ -371,13 +371,18 @@ var abp = abp || {};
};
var _loadScript = function (url, loadCallback, failCallback) {
var nonce = document.body.nonce || document.body.getAttribute('nonce');
_loadFromUrl(url, loadCallback, failCallback, function (urlInfo) {
$.get({
url: url,
dataType: 'text'
})
.done(function (script) {
$.globalEval(script);
if(nonce){
$.globalEval(script, { nonce: nonce});
}else{
$.globalEval(script);
}
urlInfo.succeed();
})
.fail(function () {

Loading…
Cancel
Save