When it comes to web security, having a good content security policy (CSP) is the most effective way to prevent a wide range of attacks on your website. CSP has been around for more than a decade now: w3.org states that the first draft was published in 2011 and Firefox adopted CSP that same year. However, although a CSP is incredibly effective, the internet is reluctant to adopt this technology. This paper from 2016 about insecure content security policies tells that at the time of the study, about 99.34% had a CSP policy that offered no protection against XSS. There is a very simple reason why so few websites have this: It's very difficult to build a website that can comply with the strictness of a secure CSP. In this post, I'm going to show you how you can deal with your inline styles in an unintrusive way in .NET 6 and up.
Introduction to CSP
In short: A CSP tells the browser where it is allowed to read information from and where it is allowed to send information to. With a CSP, you can tell the browser from which sources it can download scripts, stylesheets, fonts and more and you can tell the browser which URLs it can post forms to among other things.
You do this to prevent common attacks on your website, like cross site scripting. With a well-defined CSP, hackers are unable to inject scripts and stylesheets into your website.
One of the rules for a secure CSP, is that you should disallow inline scripts and styles. inline scripts and styles are very commonly used by hackers when they try to attack your website. However, disabling these features may introduce some challenges for developers. As a developer, you'll be unable to use any of the following:
<!-- You cannot use inline javascript -->
<a href="javascript:alert('Hello World')">Click me</a>
<!-- You also cannot use inline styles -->
<div style="background-image: url('https://example.com/my-image.jpg');"></div>
There is an exception to this rule however: You can use styles and scripts from <style>
and <script>
tags, as long as you add a nonce or a hash. A nonce is a random sequence of characters that uniquely identifies an inline resource. A nonce changes every time you request a page from the server. A hash is a summary of the content of an inline resource. It's a checksum that allows the browser to check if the content of the resource in the browser matches the content of the resource when it was sent from the server.
⚠️ NOTE |
---|
A good CSP header doesn't mean your website is safe from all attacks. Always make sure you follow the security guidelines that are proposed by the framework that you use. You can check if your website has all required security headers in place with tools like the Mozilla Observatory. |
Making inline styles compliant
To make inline styles CSP compliant, I made use of the exception for inline styles: I transformed inline styles to style tags so I could apply a nonce to them.
I had some restrictions to work with:
-
<style>
tags must be placed inside the<head>
- Frontend developers must be able to apply safe inline styles without thoroughly understanding C# or Razor pages
- Razor partials must be reusable more than once on a single page
- The solution must work everywhere, no exceptions
The setup
Let's take a basic blog page to illustrate the concept. I might have some views like this:
MyBlogPage.cshtml
@model MyBlogPage
@{
Layout = "Shared/_Layout.cshtml";
}
<header style="background-image: url('@Model.HeaderImageUrl')">
<h1>@Model.Title</h1>
</header>
<div>
@Model.BodyContent
</div>
_Layout.cshtml
<html>
<head>
</head>
<body>
<vc:NavBar />
<main>
@RenderBody()
</main>
<vc:Footer />
</body>
</html>
As you can see, the header has an inline style and is therefore not CSP compliant. The viewcomponents NavBar
and Footer
might also contain inline styles.
The first step is to intercept the inline styles with a tag helper:
// 👇The target of this tag helper is any tag with a 'style' attribute.
[HtmlTargetElement(Attributes = "style")]
public class InlineStyleTagHelper : TagHelper
{
private readonly IHttpContextAccessor _httpContextAccessor;
public InlineStyleTagHelper(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
// 👇 Store the value of the inline style in this property so we can conveniently refer to it in the logic
// This also ensures that the style attribute doesn't get written to the output
[HtmlAttributeName("style")]
public string Style { get; set; } = null!;
public override void Process(TagHelperContext context, TagHelperOutput output)
{
// 👇 When using razor for previews in the backoffice, you'll want to skip this. Thanks to huwred in the Umbraco discord for this suggestion.
if (_httpContextAccessor.HttpContext.Request.IsBackOfficeRequest())
{
output.Attributes.Add(new TagHelperAttribute("style", new HtmlString(Style)));
return;
}
// 👇 A random class name ensures that we can refer to this exact element later
string randomClass = "is-" + Guid.NewGuid().ToString();
// 👇 A 'class' attribute may or may not already be present.
// We read the contents of the existing class attribute and append our random class to the list.
var existingAttribute = output.Attributes.FirstOrDefault(a => a.Name == "class");
string valueString;
if (existingAttribute?.Value is HtmlString valueHtmlString && !string.IsNullOrWhiteSpace(valueHtmlString.Value))
{
valueString = WebUtility.HtmlDecode(valueHtmlString.Value);
valueString += " ";
}
else
{
valueString = string.Empty;
}
valueString += randomClass;
var newAttribute = new TagHelperAttribute("class", new HtmlString(valueString));
// 👇 We must not forget to actually replace the class attribute with our new one.
if (existingAttribute is not null) output.Attributes.Remove(existingAttribute);
output.Attributes.Add(newAttribute);
// 👇 The inline styles are stored inside the http context for later reference.
var styles = _httpContextAccessor.HttpContext!.InlineStyles();
styles.Add(randomClass, Style);
}
}
I used some helper objects and extensions for convenience:
InlineStyleCollection.cs
internal class InlineStyleCollection : Dictionary<string, string>
{
internal IHtmlContent ToHtmlString()
{
// 👇 The class is the key and the inline styles are the value. concatenate the inline styles into one string so they can be rendered on a page
StringBuilder sb = new();
foreach ((var key, var value) in this)
{
sb.Append($".{key}{{{value}}}");
}
return new HtmlString(sb.ToString());
}
}
InlineStyleExtensions.cs
public static class InlineStyleExtensions
{
internal static InlineStyleCollection InlineStyles(this HttpContext context)
{
var result = context.Features.Get<InlineStyleCollection>();
if (result is null)
{
result = new InlineStyleCollection();
context.Features.Set(result);
}
return result;
}
// 👇 Http context accessor is required to work with this tag helper, so we have to make sure that that service is available in the DI-container.
internal static IServiceCollection AddInlineStyle(this IServiceCollection services)
{
return services.AddHttpContextAccessor();
}
// A convenience method to render the css makes the process easier to understand for readers
public static IHtmlContent RenderInlineStyles(this RazorPage page)
{
return page.Context.InlineStyles().ToHtmlString();
}
// To prevent rendering content that we don't need, a convenience method helps to check if any inline styles were used.
public static bool HasInlineStyles(this RazorPage page)
{
return page.Context.InlineStyles().Any();
}
}
This was all the logic that I needed to make the tag helper work the way I wanted to. Next, I applied the tag helper like this:
_layoutv2.cshtml
<html>
<head>
@if(this.HasInlineStyles())
{
<style nonce="MyRandomNonce">
@this.RenderInlineStyles()
</style>
}
</head>
<body>
<vc:NavBar />
<main>
@RenderBody()
</main>
<vc:Footer />
</body>
</html>
It's not quite finished yet
With these changes, I could keep using inline styles like normal; a frontend developer wouldn't even notice the difference. It still wasn't quite working yet though:
- ✅
<style>
tags must be placed inside the<head>
- ✅ Frontend developers must be able to apply safe inline styles without thoroughly understanding C# or Razor pages
- ✅ Razor partials must be reusable more than once on a single page
- ❌ The solution must work everywhere, no exceptions
When I ran this solution, I noticed that the navbar and footer inline styles had disappeared, but didn't show up in the style tag in the head. This is because the <head>
was rendered before the NavBar and the Footer. I had to make another change to my layout to check the box on the last requirement: I had to separate the <head>
from the <body>
. This is what my layout looks like in the end:
_Layoutv3.cshtml
<html>
<head>
@if(this.HasInlineStyles())
{
<style nonce="MyRandomNonce">
@this.RenderInlineStyles()
</style>
}
</head>
@RenderBody()
</html>
_LayoutBody.cshtml
@{
Layout = "Shared/_Layoutv3.cshtml"
}
<body>
<vc:NavBar />
<main>
@RenderBody()
</main>
<vc:Footer />
</body>
And I had to make one small change to the blog view:
@model MyBlogPage
@{
Layout = "Shared/_LayoutBody.cshtml";
}
@*The rest of the content is omitted for brevity*@
The order of execution is now:
- MyBlogPage.cshtml
- _LayoutBody.cshtml
- _Layout.cshtml
And that means that I can now use inline styles everywhere!
Final thoughts
I understand that it's very difficult to build a website such that it's CSP compliant. I hope that this solution may help some of you out to make CSP-compliance somewhat more accessible. The beauty in this solution is in the non-intrusive nature. To a frontend developer, the inline style looks exactly the same so they don't have to change their workflow. The only downside in my opinion is the decapitation of the layout, which feels somewhat unnatural to me, but I'm willing to live with that if the tradeoff is this convenience.
What's your opinion on this? Is it a good thing that developers don't have to change their workflow and don't have to think about CSP compliance in their day-to-day work or does it make developers more lazy and uneducated? Leave a comment and let me know!
For now I'll say: Thanks for reading and I hope to see you in my next blog post! 😊
Top comments (2)
Cool stuff Dennis! Thanks for sharing!
Could you avoid the layout decapitation by putting the inline styles in the start of your
tag instead?I don't believe there is any performance hits, or repaints caused by this, as long as the inline style is the first element.
Hey! part of you message seems to have disappeared. I'm assuming you're asking if I could place the inline styles in the start of the body tag? I think that's possible. As far as I know, they would work just the same.
The requirement to put the inline styles in the head came from within my organization and I don't know enough about html tags to know what the reasoning is behind this.
But yeah, rendering the inline styles inside the body tag would remove the need to decapitate the layout. That being said: if you have a navbar and/or a footer in the layout, you'll still have to render those first and then render the inline styles.