From d7b6a4f67ecc5c1e194ab67bb4c666cffa0d7b0f Mon Sep 17 00:00:00 2001 From: Dustin Masters Date: Sun, 21 Jan 2018 14:59:32 -0800 Subject: [PATCH 1/4] Add support for script nonce attributes --- src/React.AspNet/HtmlHelperExtensions.cs | 54 +++++++++------- src/React.Core/IReactEnvironment.cs | 8 ++- src/React.Core/IReactSiteConfiguration.cs | 16 ++++- src/React.Core/ReactSiteConfiguration.cs | 18 ++++++ .../Mvc/HtmlHelperExtensionsTests.cs | 62 ++++++++++++++++++- 5 files changed, 129 insertions(+), 29 deletions(-) diff --git a/src/React.AspNet/HtmlHelperExtensions.cs b/src/React.AspNet/HtmlHelperExtensions.cs index b83f1ad6c..903f8bdd9 100644 --- a/src/React.AspNet/HtmlHelperExtensions.cs +++ b/src/React.AspNet/HtmlHelperExtensions.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2014-Present, Facebook, Inc. * All rights reserved. * @@ -8,8 +8,6 @@ */ using System; -using React.Exceptions; -using React.TinyIoC; #if LEGACYASPNET using System.Web; @@ -129,16 +127,7 @@ public static IHtmlString ReactWithInit( } 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 + GetScriptTag(reactComponent.RenderJavaScript()).ToString()); } finally { @@ -155,23 +144,40 @@ 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) + { + string nonce = Environment.Configuration.ScriptNonceProvider(); + tag.Attributes.Add("nonce", nonce ); + } + + return new HtmlString(tag.ToString()); #else var tag = new TagBuilder("script"); tag.InnerHtml.AppendHtml(script); - return tag; -#endif - } - finally + + if (Environment.Configuration.ScriptNonceProvider != null) { - Environment.ReturnEngineToPool(); + tag.Attributes.Add("nonce", Environment.Configuration.ScriptNonceProvider()); } + + return tag; +#endif } } } diff --git a/src/React.Core/IReactEnvironment.cs b/src/React.Core/IReactEnvironment.cs index 599ffd1f8..67991360f 100644 --- a/src/React.Core/IReactEnvironment.cs +++ b/src/React.Core/IReactEnvironment.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2014-Present, Facebook, Inc. * All rights reserved. * @@ -7,7 +7,6 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -using System; namespace React { @@ -110,5 +109,10 @@ public interface IReactEnvironment /// Returns the currently held JS engine to the pool. (no-op if engine pooling is disabled) /// void ReturnEngineToPool(); + + /// + /// Gets the site-wide configuration. + /// + IReactSiteConfiguration Configuration { get; } } } diff --git a/src/React.Core/IReactSiteConfiguration.cs b/src/React.Core/IReactSiteConfiguration.cs index 8c3eed6da..b414e5d25 100644 --- a/src/React.Core/IReactSiteConfiguration.cs +++ b/src/React.Core/IReactSiteConfiguration.cs @@ -8,8 +8,8 @@ */ using System; -using Newtonsoft.Json; using System.Collections.Generic; +using Newtonsoft.Json; namespace React { @@ -193,5 +193,19 @@ public interface IReactSiteConfiguration /// /// IReactSiteConfiguration SetExceptionHandler(Action handler); + + /// + /// 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. + /// + Func ScriptNonceProvider { get; set; } + + /// + /// 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. + /// + /// + /// + IReactSiteConfiguration SetScriptNonceProvider(Func provider); } } diff --git a/src/React.Core/ReactSiteConfiguration.cs b/src/React.Core/ReactSiteConfiguration.cs index 879a92db9..dd735b1c9 100644 --- a/src/React.Core/ReactSiteConfiguration.cs +++ b/src/React.Core/ReactSiteConfiguration.cs @@ -326,5 +326,23 @@ public IReactSiteConfiguration SetExceptionHandler(Action + /// 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. + /// + public Func ScriptNonceProvider { get; set; } + + /// + /// 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. + /// + /// + /// + public IReactSiteConfiguration SetScriptNonceProvider(Func provider) + { + ScriptNonceProvider = provider; + return this; + } } } diff --git a/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs b/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs index 1b2a59619..b1937b58c 100644 --- a/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs +++ b/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs @@ -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 { @@ -20,9 +22,10 @@ public class HtmlHelperExtensionsTests /// This is only required because can not be /// injected :( /// - private Mock ConfigureMockEnvironment() + private Mock ConfigureMockEnvironment(IReactSiteConfiguration configuration = null) { var environment = new Mock(); + environment.Setup(x => x.Configuration).Returns(configuration ?? new ReactSiteConfiguration()); AssemblyRegistration.Container.Register(environment.Object); return environment; } @@ -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(); + component.Setup(x => x.RenderHtml(false, false, null)).Returns("HTML"); + component.Setup(x => x.RenderJavaScript()).Returns("JS"); + + var config = new Mock(); + + 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 + "", + 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 + "", + result.ToString() + ); + } + [Fact] public void EngineIsReturnedToPoolAfterRender() { From 72a2929eff4dc4d4c09b81171db3393b49a017c9 Mon Sep 17 00:00:00 2001 From: Dustin Masters Date: Sun, 18 Feb 2018 20:32:55 -0800 Subject: [PATCH 2/4] Fix spacing nit --- src/React.AspNet/HtmlHelperExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/React.AspNet/HtmlHelperExtensions.cs b/src/React.AspNet/HtmlHelperExtensions.cs index 903f8bdd9..0e0bb61d4 100644 --- a/src/React.AspNet/HtmlHelperExtensions.cs +++ b/src/React.AspNet/HtmlHelperExtensions.cs @@ -163,7 +163,7 @@ private static IHtmlString GetScriptTag(string script) if (Environment.Configuration.ScriptNonceProvider != null) { string nonce = Environment.Configuration.ScriptNonceProvider(); - tag.Attributes.Add("nonce", nonce ); + tag.Attributes.Add("nonce", nonce); } return new HtmlString(tag.ToString()); From 7f7851c540f38bd67a0140180cc1b9e7c7e10dd9 Mon Sep 17 00:00:00 2001 From: Dustin Masters Date: Sun, 18 Feb 2018 21:29:26 -0800 Subject: [PATCH 3/4] Fix ReactWithInit in .NET Core https://developercommunity.visualstudio.com/content/problem/17287/tagbuilder-tostring-returns-the-type-of-tagbuilder.html --- src/React.AspNet/HtmlHelperExtensions.cs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/React.AspNet/HtmlHelperExtensions.cs b/src/React.AspNet/HtmlHelperExtensions.cs index 0e0bb61d4..a3d796eb2 100644 --- a/src/React.AspNet/HtmlHelperExtensions.cs +++ b/src/React.AspNet/HtmlHelperExtensions.cs @@ -8,12 +8,14 @@ */ using System; +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; @@ -127,7 +129,7 @@ public static IHtmlString ReactWithInit( } var html = reactComponent.RenderHtml(clientOnly, exceptionHandler: exceptionHandler); - return new HtmlString(html + System.Environment.NewLine + GetScriptTag(reactComponent.RenderJavaScript()).ToString()); + return new HtmlString(html + System.Environment.NewLine + RenderToString(GetScriptTag(reactComponent.RenderJavaScript()))); } finally { @@ -162,8 +164,7 @@ private static IHtmlString GetScriptTag(string script) if (Environment.Configuration.ScriptNonceProvider != null) { - string nonce = Environment.Configuration.ScriptNonceProvider(); - tag.Attributes.Add("nonce", nonce); + tag.Attributes.Add("nonce", Environment.Configuration.ScriptNonceProvider()); } return new HtmlString(tag.ToString()); @@ -177,6 +178,20 @@ private static IHtmlString GetScriptTag(string script) } return tag; +#endif + } + + // 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()) + { + source.WriteTo(writer, HtmlEncoder.Default); + return writer.ToString(); + } #endif } } From 75f4709084dff76bb8720e5a8303142198eec1eb Mon Sep 17 00:00:00 2001 From: Dustin Masters Date: Sun, 18 Feb 2018 21:30:02 -0800 Subject: [PATCH 4/4] Fix broken proptype references in mvc4 sample --- src/React.Sample.Mvc4/Content/Sample.jsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/React.Sample.Mvc4/Content/Sample.jsx b/src/React.Sample.Mvc4/Content/Sample.jsx index 8875ef8f1..97d078f09 100644 --- a/src/React.Sample.Mvc4/Content/Sample.jsx +++ b/src/React.Sample.Mvc4/Content/Sample.jsx @@ -1,4 +1,4 @@ -/** +/** * Copyright (c) 2014-Present, Facebook, Inc. * All rights reserved. * @@ -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 = { @@ -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() { @@ -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() {