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

Add support for script nonce attributes #496

Merged
merged 4 commits into from
Feb 19, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 43 additions & 22 deletions src/React.AspNet/HtmlHelperExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
* Copyright (c) 2014-Present, Facebook, Inc.
* All rights reserved.
*
Expand All @@ -8,14 +8,14 @@
*/

using System;
using React.Exceptions;
using React.TinyIoC;
using System.IO;

#if LEGACYASPNET
using System.Web;
using System.Web.Mvc;
using IHtmlHelper = System.Web.Mvc.HtmlHelper;
#else
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Mvc.Rendering;
using IHtmlString = Microsoft.AspNetCore.Html.IHtmlContent;
using Microsoft.AspNetCore.Html;
Expand Down Expand Up @@ -129,16 +129,7 @@ public static IHtmlString ReactWithInit<T>(
}
var html = reactComponent.RenderHtml(clientOnly, exceptionHandler: exceptionHandler);

#if LEGACYASPNET
var script = new TagBuilder("script")
{
InnerHtml = reactComponent.RenderJavaScript()
};
#else
var script = new TagBuilder("script");
script.InnerHtml.AppendHtml(reactComponent.RenderJavaScript());
#endif
return new HtmlString(html + System.Environment.NewLine + script.ToString());
return new HtmlString(html + System.Environment.NewLine + RenderToString(GetScriptTag(reactComponent.RenderJavaScript())));
}
finally
{
Expand All @@ -155,23 +146,53 @@ public static IHtmlString ReactInitJavaScript(this IHtmlHelper htmlHelper, bool
{
try
{
var script = Environment.GetInitJavaScript(clientOnly);
return GetScriptTag(Environment.GetInitJavaScript(clientOnly));
}
finally
{
Environment.ReturnEngineToPool();
}
}

private static IHtmlString GetScriptTag(string script)
{
#if LEGACYASPNET
var tag = new TagBuilder("script")
{
InnerHtml = script
};
return new HtmlString(tag.ToString());
var tag = new TagBuilder("script")
{
InnerHtml = script,
};

if (Environment.Configuration.ScriptNonceProvider != null)
{
tag.Attributes.Add("nonce", Environment.Configuration.ScriptNonceProvider());
}

return new HtmlString(tag.ToString());
#else
var tag = new TagBuilder("script");
tag.InnerHtml.AppendHtml(script);

if (Environment.Configuration.ScriptNonceProvider != null)
{
tag.Attributes.Add("nonce", Environment.Configuration.ScriptNonceProvider());
}

return tag;
#endif
}
finally
}

// In ASP.NET Core, you can no longer call `.ToString` on `IHtmlString`
private static string RenderToString(IHtmlString source)
{
#if LEGACYASPNET
return source.ToString();
#else
using (var writer = new StringWriter())
{
Environment.ReturnEngineToPool();
source.WriteTo(writer, HtmlEncoder.Default);
return writer.ToString();
}
#endif
}
}
}
8 changes: 6 additions & 2 deletions src/React.Core/IReactEnvironment.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
* Copyright (c) 2014-Present, Facebook, Inc.
* All rights reserved.
*
Expand All @@ -7,7 +7,6 @@
* of patent rights can be found in the PATENTS file in the same directory.
*/

using System;

namespace React
{
Expand Down Expand Up @@ -110,5 +109,10 @@ public interface IReactEnvironment
/// Returns the currently held JS engine to the pool. (no-op if engine pooling is disabled)
/// </summary>
void ReturnEngineToPool();

/// <summary>
/// Gets the site-wide configuration.
/// </summary>
IReactSiteConfiguration Configuration { get; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't there already a getter for this? ReactSiteConfiguration.Configuration or .Current or something like that?

}
}
16 changes: 15 additions & 1 deletion src/React.Core/IReactSiteConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
*/

using System;
using Newtonsoft.Json;
using System.Collections.Generic;
using Newtonsoft.Json;

namespace React
{
Expand Down Expand Up @@ -193,5 +193,19 @@ public interface IReactSiteConfiguration
/// <param name="handler"></param>
/// <returns></returns>
IReactSiteConfiguration SetExceptionHandler(Action<Exception, string, string> handler);

/// <summary>
/// A provider that returns a nonce to be used on any script tags on the page.
/// This value must match the nonce used in the Content Security Policy header on the response.
/// </summary>
Func<string> ScriptNonceProvider { get; set; }

/// <summary>
/// Sets a provider that returns a nonce to be used on any script tags on the page.
/// This value must match the nonce used in the Content Security Policy header on the response.
/// </summary>
/// <param name="provider"></param>
/// <returns></returns>
IReactSiteConfiguration SetScriptNonceProvider(Func<string> provider);
}
}
18 changes: 18 additions & 0 deletions src/React.Core/ReactSiteConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -326,5 +326,23 @@ public IReactSiteConfiguration SetExceptionHandler(Action<Exception, string, str
ExceptionHandler = handler;
return this;
}

/// <summary>
/// A provider that returns a nonce to be used on any script tags on the page.
/// This value must match the nonce used in the Content Security Policy header on the response.
/// </summary>
public Func<string> ScriptNonceProvider { get; set; }

/// <summary>
/// Sets a provider that returns a nonce to be used on any script tags on the page.
/// This value must match the nonce used in the Content Security Policy header on the response.
/// </summary>
/// <param name="provider"></param>
/// <returns></returns>
public IReactSiteConfiguration SetScriptNonceProvider(Func<string> provider)
{
ScriptNonceProvider = provider;
return this;
}
}
}
10 changes: 5 additions & 5 deletions src/React.Sample.Mvc4/Content/Sample.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/**
/**
* Copyright (c) 2014-Present, Facebook, Inc.
* All rights reserved.
*
Expand All @@ -9,8 +9,8 @@

class CommentsBox extends React.Component {
static propTypes = {
initialComments: React.PropTypes.array.isRequired,
page: React.PropTypes.number
initialComments: PropTypes.array.isRequired,
page: PropTypes.number
};

state = {
Expand Down Expand Up @@ -76,7 +76,7 @@ class CommentsBox extends React.Component {

class Comment extends React.Component {
static propTypes = {
author: React.PropTypes.object.isRequired
author: PropTypes.object.isRequired
};

render() {
Expand All @@ -92,7 +92,7 @@ class Comment extends React.Component {

class Avatar extends React.Component {
static propTypes = {
author: React.PropTypes.object.isRequired
author: PropTypes.object.isRequired
};

render() {
Expand Down
62 changes: 60 additions & 2 deletions tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
* of patent rights can be found in the PATENTS file in the same directory.
*/

using System;
using System.Security.Cryptography;
using Moq;
using Xunit;
using React.Web.Mvc;
using Xunit;

namespace React.Tests.Mvc
{
Expand All @@ -20,9 +22,10 @@ public class HtmlHelperExtensionsTests
/// This is only required because <see cref="HtmlHelperExtensions"/> can not be
/// injected :(
/// </summary>
private Mock<IReactEnvironment> ConfigureMockEnvironment()
private Mock<IReactEnvironment> ConfigureMockEnvironment(IReactSiteConfiguration configuration = null)
{
var environment = new Mock<IReactEnvironment>();
environment.Setup(x => x.Configuration).Returns(configuration ?? new ReactSiteConfiguration());
AssemblyRegistration.Container.Register(environment.Object);
return environment;
}
Expand Down Expand Up @@ -54,6 +57,61 @@ public void ReactWithInitShouldReturnHtmlAndScript()
);
}

[Fact]
public void ScriptNonceIsReturned()
{
string nonce;
using (var random = new RNGCryptoServiceProvider())
{
byte[] nonceBytes = new byte[16];
random.GetBytes(nonceBytes);
nonce = Convert.ToBase64String(nonceBytes);
}

var component = new Mock<IReactComponent>();
component.Setup(x => x.RenderHtml(false, false, null)).Returns("HTML");
component.Setup(x => x.RenderJavaScript()).Returns("JS");

var config = new Mock<IReactSiteConfiguration>();

var environment = ConfigureMockEnvironment(config.Object);

environment.Setup(x => x.Configuration).Returns(config.Object);
environment.Setup(x => x.CreateComponent(
"ComponentName",
new { },
null,
false,
false
)).Returns(component.Object);

// without nonce
var result = HtmlHelperExtensions.ReactWithInit(
htmlHelper: null,
componentName: "ComponentName",
props: new { },
htmlTag: "span"
);
Assert.Equal(
"HTML" + System.Environment.NewLine + "<script>JS</script>",
result.ToString()
);

config.Setup(x => x.ScriptNonceProvider).Returns(() => nonce);

// with nonce
result = HtmlHelperExtensions.ReactWithInit(
htmlHelper: null,
componentName: "ComponentName",
props: new { },
htmlTag: "span"
);
Assert.Equal(
"HTML" + System.Environment.NewLine + "<script nonce=\"" + nonce + "\">JS</script>",
result.ToString()
);
}

[Fact]
public void EngineIsReturnedToPoolAfterRender()
{
Expand Down