Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Content Security Policy with script-src 'self' prevents Application Insights javascript running #1443

Closed
Spencerooni opened this issue Oct 26, 2017 · 22 comments
Assignees
Milestone

Comments

@Spencerooni
Copy link

In Shared/_Layout I have added the following line within the <head> section of my application:

@Html.Raw(JavaScriptSnippet.FullScript)

Which correctly outputs the Application Insights javascript, inline. However, my application has the following header in the response for security reasons...

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

... which prevents inline javascript. Therefore, I receive the following error in the browser console (chrome):

Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self'". Either the 'unsafe-inline' keyword, a hash ('sha256-od4GZDd/FJpzTaUjnBJEZbKvWwfP3SPsG+UsfNdoDpc='), or a nonce ('nonce-...') is required to enable inline execution.

@ch53k
Copy link

ch53k commented Nov 3, 2017

I created a quick and dirty work around for this issue in my project.

NOTE: I've updated the post to fix a bug. The original post stored the nonce in a static (I was clearly not thinking when I wrote it). It is now stored in the request context Items property. I also wrote a more detailed blog post today, which can be found at: http://robanstett.com/blog/asp-core-ai-csp/

First I added a Content-Security Policy by adding the following to my startup (this will probably be different for you. This must be added prior to app.UseMvc.

app.Use(async (ctx, next) =>
{
    var nonce = Guid.NewGuid().ToString("N");
    ctx.Response.Headers.Add("Content-Security-Policy",
        $"script-src https://az416426.vo.msecnd.net 'self' \'nonce-{nonce}\'");

    ctx.Items["csp-nonce"] = nonce;
    await next();

Next I created a helper class to get the AI script and add to the header of it the Nonce Code I put in the Content-Security-Policy.

Here is a the class:

using Microsoft.ApplicationInsights.AspNetCore;
namespace Services
{
    public class ApplicationInsightsJsHelper
    {
        private readonly IHttpContextAccessor _httpContext;
        private readonly JavaScriptSnippet _aiJavaScriptSnippet;
        
        public ApplicationInsightsJsHelper(IHttpContextAccessor httpContext, JavaScriptSnippet aiJavaScriptSnippet)
        {
            _httpContext = httpContext;
            _aiJavaScriptSnippet = aiJavaScriptSnippet;
        }

        public string Script
        {
            get
            {
                var js = _aiJavaScriptSnippet.FullScript;
                const string scriptTagStart = @"<script type=""text/javascript"">";
                var scriptTagStartWithNonce = $"<script type=\"text/javascript\" nonce=\"{_httpContext.HttpContext.Items["csp-nonce"]}\">";
                var script = js.Replace(scriptTagStart, scriptTagStartWithNonce);
                return script;
            }
        }
    }
}

@Philo
Copy link

Philo commented Nov 10, 2017

I have a similar issue but would prefer that a solution was considered that would allow the AI snippet to populate the instrumentation key (and authicatedUserContext) from variables selected via the DOM:

I have a separate script like below:

var aiScript = document.querySelector('#ai-script-snippet');
if(aiScript) {
    var key = key : aiScript.getAttribute('data-ai-key');
    var userId = aiScript.getAttribute('data-ai-userid');

    if(key) {
        var appInsights=window.appInsights||function(config){
            function i(config){t[config]=function(){var i=arguments;t.queue.push(function(){t[config].apply(t,i)})}}var t={config:config},u=document,e=window,o="script",s="AuthenticatedUserContext",h="start",c="stop",l="Track",a=l+"Event",v=l+"Page",y=u.createElement(o),r,f;y.src=config.url||"https://az416426.vo.msecnd.net/scripts/a/ai.0.js";u.getElementsByTagName(o)[0].parentNode.appendChild(y);try{t.cookie=u.cookie}catch(p){}for(t.queue=[],t.version="1.0",r=["Event","Exception","Metric","PageView","Trace","Dependency"];r.length;)i("track"+r.pop());return i("set"+s),i("clear"+s),i(h+a),i(c+a),i(h+v),i(c+v),i("flush"),config.disableExceptionTracking||(r="onerror",i("_"+r),f=e[r],e[r]=function(config,i,u,e,o){var s=f&&f(config,i,u,e,o);return s!==!0&&t["_"+r](config,i,u,e,o),s}),t
        }({
            instrumentationKey: key
        });

        window.appInsights=appInsights;

        if(userId) {
            appInsights.setAuthenticatedUserContext(userId);
        }

        appInsights.trackPageView();
    }
}

This is a separate script that is then satisfied by the 'self' directive, I then include an id selector used to query an element that will contain the values needed to populate the AI snippet, this can then target any element to query the values:

<html lang="en" id="ai-script-snippet" data-ai-key="@TelemetryConfiguration.Active.InstrumentationKey" data-ai-userid="@(User.Identity.IsAuthenticated ? User.Identity.Name : null)">
<script src="/scripts/my-ai-snippet.js"></script>
</html>

or

<html>
<script id="ai-script-snippet" data-ai-key="@TelemetryConfiguration.Active.InstrumentationKey" data-ai-userid="@(User.Identity.IsAuthenticated ? User.Identity.Name : null)" src="/scripts/my-ai-snippet.js"></script>
</html>

or even (perhaps this could work as an update to this library?)

<html>
<script id="ai-script-snippet" data-ai-key="@TelemetryConfiguration.Active.InstrumentationKey" data-ai-userid="@(User.Identity.IsAuthenticated ? User.Identity.Name : null)">
var aiScript = document.querySelector('#ai-script-snippet');
if(aiScript) {
    var key = key : aiScript.getAttribute('data-ai-key');
    var userId = aiScript.getAttribute('data-ai-userid');

    if(key) {
        var appInsights=window.appInsights||function(config){
            function i(config){t[config]=function(){var i=arguments;t.queue.push(function(){t[config].apply(t,i)})}}var t={config:config},u=document,e=window,o="script",s="AuthenticatedUserContext",h="start",c="stop",l="Track",a=l+"Event",v=l+"Page",y=u.createElement(o),r,f;y.src=config.url||"https://az416426.vo.msecnd.net/scripts/a/ai.0.js";u.getElementsByTagName(o)[0].parentNode.appendChild(y);try{t.cookie=u.cookie}catch(p){}for(t.queue=[],t.version="1.0",r=["Event","Exception","Metric","PageView","Trace","Dependency"];r.length;)i("track"+r.pop());return i("set"+s),i("clear"+s),i(h+a),i(c+a),i(h+v),i(c+v),i("flush"),config.disableExceptionTracking||(r="onerror",i("_"+r),f=e[r],e[r]=function(config,i,u,e,o){var s=f&&f(config,i,u,e,o);return s!==!0&&t["_"+r](config,i,u,e,o),s}),t
        }({
            instrumentationKey: key
        });

        window.appInsights=appInsights;

        if(userId) {
            appInsights.setAuthenticatedUserContext(userId);
        }

        appInsights.trackPageView();
    }
}
</script>
</html>

..and then include a fixed csp hash of 'sha256-j0z5Z6xOoSHTeg6zVPMoHNt5D5v+7IWgT9m5uC9mrGg=' for the script

@Philo
Copy link

Philo commented Jan 29, 2018

Is there any official response on this?

The automatic light-up feature of AppInsights in core 2.0 injects a code snippet that causes script errors when using a strict CSP policy.

Not having a method to disable the script injection without a big hammer (IHostingStartup) is nuts and a big issue and concern for those wanting to use AppInsights and Core 2.0 with a secure content security policy.

@shunty-gh
Copy link

This may well be frowned upon and get me barred from the "acceptable code" club but I'm using a slightly modified, but equally hacky, approach.

I added the script manually with a nonce/hash attribute on the tag (as described in various C# + CSP articles) but then found that the AI startup code still insisted on putting a second copy of the script into the page - thus breaking the CSP. There doesn't seem to be a (straightforward) way of preventing it doing that so I used a bit of reflection in the Startup.ConfigureServices method to replace the script:

FieldInfo field = typeof(Microsoft.ApplicationInsights.AspNetCore.JavaScriptSnippet)
        .GetField( "Snippet", BindingFlags.Static | BindingFlags.NonPublic);
field.SetValue(null, "");

This allows the rest of the AI stuff to work as normal and just prevents a second copy of the snippet in my page. It's dirty and not strictly future proof but it works until they come up with a more acceptable solution.

@Philo
Copy link

Philo commented Feb 12, 2018

Yesterday crypto-mining exploit against Browsealoud further outlines the need for a robust solution that allows for secure inclusion of scripts such as AppInsights. The irony here is that the very inclusion of AppInsights allowed me to investigate the extent of the exploit on sites I maintain, but the need for a lax Content Security Policy for AppInsights also allowed for the Browsealoud exploit to happen.

This is a wake up calls for a lot of people that read about CSP and did nothing, and for software and script vendors to take heed of the warnings of this incident to better secure their own resources and also support browser mechanics to aid in the protection of websites using these scripts.

@qJake
Copy link

qJake commented Jun 13, 2018

I had the same issue in trying to implement CSP on my site. The workaround from @shunty-gh worked great, and should continue to work until Microsoft changes a namespace, type, or field name.

@ianrathbone
Copy link

Thanks @shunty-gh I've applied your workaround too

@garethterrace
Copy link

I'm having a related issue implementing a strict-dynamic, nonce based CSP with no host whitelist. This workaround won't work for a strict-dynamic v3 compliant CSP as any whitelists are ignored.

The injected JS snippet builds another script tag which means we can't easily pass a nonce into the script when it's included in the page. I can manually do it using some convoluted string parsing but this is fragile if the generated snippet ever changes the script it writes.

Can I raise a feature request to support CSP nonces in the JS snippet?

@qJake
Copy link

qJake commented Jun 28, 2019

@garethterrace I think that raises a larger question of, if this were to be implemented, how and where would the nonce value be passed through the request pipeline so that it is available throughout the various ASP.NET components? Sure, getting it to work with App Insights is fine, but if there are multiple <script> tags on a page, all need to share the same nonce value.

I think this is something that has to be supported natively throughout ASP.NET Core (if it's not planned or in progress already), and then the Application Insights library needs to take advantage of it.

@TimothyMothra TimothyMothra transferred this issue from microsoft/ApplicationInsights-aspnetcore Dec 4, 2019
@stemixreal
Copy link

I'm new to CSP and had the same problem. Adding the sha to scrpit-src solved it quickly and easily. You'd have to change the sha if the script was changed but that isn't often afaik. I'm not up to speed on dynamic script in csp 3 yet, if it allows only nonces the sha won't work.

Like I said I'm new to this side of things so could very easily be wrong. Checking my headers with various tools seems to confirm what I think you can do. Please feel free to correct me if I'm talking out of my hat, I'm keen to learn where I'm mistaken.

@qJake
Copy link

qJake commented Feb 7, 2020

Any update on this? Does ASP.NET Core handle script nonces out of the box, or how is CSP being handled going forward with regards to the App Insights injected script tag?

@TimothyMothra
Copy link
Member

For reference, the snipped comes from here:

I'm reading up on CSP and I'm not aware of any mechanism in AspNetCore to provide a nonce for us.

So the immediate question I have is if you implement your own nonce, where would you store it that we could read it and apply it to the script tag?

@qJake
Copy link

qJake commented Feb 8, 2020

@TimothyMothra I have a home-grown nonce solution in my ASP.NET Core projects. Basically, the way it works is, I generate a nonce value early on in the request pipeline (pseudo-randomly), write it as part of the CSP header, and set it in the HttpContext.Items collection. Then, I wrote a tag helper called auto-nonce="true" that, if set, will read the same key out of HttpContext.Items and apply the correct nonce value scoped to that particular request onto the specified HTML tag. (It's valid on <script> and <style> tags and possibly some others I'm not thinking of.)

It works very well, except when stuff wants to write <script> tags onto the page without my control. 😉


One possible solution would be, if we had the capability to do something like this:

<script auto-nonce="true">
    @Microsoft.ApplicationInsights.AspNetCore.JavaScriptSnippet.WriteSnippet(false);
</script>

With the signature of that method being something like: string WriteSnippet(bool includeScriptTags = true)

This method, if called, would also somehow need to detect that it exists or was invoked on a Razor page, and not write the automatic snippet in that case. (Not sure it's possible to detect if this method exists in the Razor compilation context ahead of time or not.)


Another solution would simply be to read a specific config value from the ASP.NET Core config provider (we already have ApplicationInsights:InstrumentationKey, after all) - something like ApplicationInsights:SuppressScriptTag (true/false) for example.

This may work well in conjunction with the manual snippet writing above.

@TimothyMothra
Copy link
Member

@qJake thanks for your detailed reply!
This all sounds very reasonable to me. I need to review this one of our architects before we proceed.
I wanted to reply and set the expectation that there won't be a quick turn around on our end but we are taking this seriously.

@dancundy
Copy link

@TimothyMothra I have a home-grown nonce solution in my ASP.NET Core projects. Basically, the way it works is, I generate a nonce value early on in the request pipeline (pseudo-randomly), write it as part of the CSP header, and set it in the HttpContext.Items collection. Then, I wrote a tag helper called auto-nonce="true" that, if set, will read the same key out of HttpContext.Items and apply the correct nonce value scoped to that particular request onto the specified HTML tag. (It's valid on <script> and <style> tags and possibly some others I'm not thinking of.)

It works very well, except when stuff wants to write <script> tags onto the page without my control. 😉

One possible solution would be, if we had the capability to do something like this:

<script auto-nonce="true">
    @Microsoft.ApplicationInsights.AspNetCore.JavaScriptSnippet.WriteSnippet(false);
</script>

With the signature of that method being something like: string WriteSnippet(bool includeScriptTags = true)

This method, if called, would also somehow need to detect that it exists or was invoked on a Razor page, and not write the automatic snippet in that case. (Not sure it's possible to detect if this method exists in the Razor compilation context ahead of time or not.)

Another solution would simply be to read a specific config value from the ASP.NET Core config provider (we already have ApplicationInsights:InstrumentationKey, after all) - something like ApplicationInsights:SuppressScriptTag (true/false) for example.

This may work well in conjunction with the manual snippet writing above.

This solution would be great. I use a also helper lib to manage my nonces.

@TimothyMothra
Copy link
Member

So the plan is to add a new public property:

  • JavaScriptSnippet.FullScript (existing)
    Provides the <script> tag and the script body
  • JavaScriptSnippet.ScriptBody (new)
    Will provide only the script body

I'm going to try to get a PR for this today or tomorrow.

@qJake
Copy link

qJake commented Feb 26, 2020

@TimothyMothra This is awesome. And just to come full circle so I understand how this is going to work... if we look at the client-side snippet documentation, it says to add this line:

@Html.Raw(JavaScriptSnippet.FullScript)

Which, for this PR, we can optionally change to:

<script auto-nonce="true"> @* Or whatever your nonce strategy is... *@
    @Html.Raw(JavaScriptSnippet.ScriptBody)
</script>

Correct? 😁

@TimothyMothra
Copy link
Member

Correct!
The idea is to give you full control over that script tag.
I have a PR for this now, but I was waiting to get some feedback.

@qJake
Copy link

qJake commented Feb 26, 2020

I love it! I can't think of any scenarios where this wouldn't be flexible.

And I suppose that if someone decided to define the script tag as, say, <script async defer> and that caused telemetry issues... that's on them (and/or that's a documentation issue that should be noted as such).

@TimothyMothra
Copy link
Member

merged. Expect this change to be available in 2.14-beta3. Should be released this or next week.

@chinwobble
Copy link

Hi I'm having trouble getting this working with these packages.

<PackageReference Include="NWebsec.AspNetCore.Middleware" Version="3.0.0" />
<PackageReference Include="NWebsec.AspNetCore.Mvc.TagHelpers" Version="3.0.0" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.17.0" />

In my startup code I have these csp headers configured:

app.UseCsp(options =>
            {
                options.ScriptSources(s =>
                {
                    s.StrictDynamic()
                    .CustomSources("https:", "http:")
                    .Self()
                    .UnsafeInline();
                    //s.UnsafeInline().CustomSources("https://az416426.vo.msecnd.net");
                });
            });

After that I updated my cshtml to this:

<script nws-csp-add-nonce="true">
  @Html.Raw(JavaScriptSnippet.ScriptBody)
</script>

After I load the page the following snippet is added.

<script src="https://az416426.vo.msecnd.net/scripts/b/ai.2.min.js"></script>

However it has no nonce so csp refuses to load it.

Content Security Policy: The page’s settings blocked the loading of a resource at https://az416426.vo.msecnd.net/scripts/b/ai.2.min.js (“script-src”).

Am I missing something?

@stemixreal
Copy link

stemixreal commented May 6, 2021

@chinwobble

Add "https://az416426.vo.msecnd.net" to script-src or use a SHA directive. Nonces seemed too difficult to me to get working but the other 2 options work fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests