Skip to content

Commit 5f5f65d

Browse files
Add support for script nonce attributes (#496)
* Add support for script nonce attributes * Fix spacing nit * Fix ReactWithInit in .NET Core https://developercommunity.visualstudio.com/content/problem/17287/tagbuilder-tostring-returns-the-type-of-tagbuilder.html * Fix broken proptype references in mvc4 sample
1 parent e5cb5d8 commit 5f5f65d

File tree

6 files changed

+147
-32
lines changed

6 files changed

+147
-32
lines changed

src/React.AspNet/HtmlHelperExtensions.cs

+43-22
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/*
1+
/*
22
* Copyright (c) 2014-Present, Facebook, Inc.
33
* All rights reserved.
44
*
@@ -8,14 +8,14 @@
88
*/
99

1010
using System;
11-
using React.Exceptions;
12-
using React.TinyIoC;
11+
using System.IO;
1312

1413
#if LEGACYASPNET
1514
using System.Web;
1615
using System.Web.Mvc;
1716
using IHtmlHelper = System.Web.Mvc.HtmlHelper;
1817
#else
18+
using System.Text.Encodings.Web;
1919
using Microsoft.AspNetCore.Mvc.Rendering;
2020
using IHtmlString = Microsoft.AspNetCore.Html.IHtmlContent;
2121
using Microsoft.AspNetCore.Html;
@@ -129,16 +129,7 @@ public static IHtmlString ReactWithInit<T>(
129129
}
130130
var html = reactComponent.RenderHtml(clientOnly, exceptionHandler: exceptionHandler);
131131

132-
#if LEGACYASPNET
133-
var script = new TagBuilder("script")
134-
{
135-
InnerHtml = reactComponent.RenderJavaScript()
136-
};
137-
#else
138-
var script = new TagBuilder("script");
139-
script.InnerHtml.AppendHtml(reactComponent.RenderJavaScript());
140-
#endif
141-
return new HtmlString(html + System.Environment.NewLine + script.ToString());
132+
return new HtmlString(html + System.Environment.NewLine + RenderToString(GetScriptTag(reactComponent.RenderJavaScript())));
142133
}
143134
finally
144135
{
@@ -155,23 +146,53 @@ public static IHtmlString ReactInitJavaScript(this IHtmlHelper htmlHelper, bool
155146
{
156147
try
157148
{
158-
var script = Environment.GetInitJavaScript(clientOnly);
149+
return GetScriptTag(Environment.GetInitJavaScript(clientOnly));
150+
}
151+
finally
152+
{
153+
Environment.ReturnEngineToPool();
154+
}
155+
}
156+
157+
private static IHtmlString GetScriptTag(string script)
158+
{
159159
#if LEGACYASPNET
160-
var tag = new TagBuilder("script")
161-
{
162-
InnerHtml = script
163-
};
164-
return new HtmlString(tag.ToString());
160+
var tag = new TagBuilder("script")
161+
{
162+
InnerHtml = script,
163+
};
164+
165+
if (Environment.Configuration.ScriptNonceProvider != null)
166+
{
167+
tag.Attributes.Add("nonce", Environment.Configuration.ScriptNonceProvider());
168+
}
169+
170+
return new HtmlString(tag.ToString());
165171
#else
166172
var tag = new TagBuilder("script");
167173
tag.InnerHtml.AppendHtml(script);
174+
175+
if (Environment.Configuration.ScriptNonceProvider != null)
176+
{
177+
tag.Attributes.Add("nonce", Environment.Configuration.ScriptNonceProvider());
178+
}
179+
168180
return tag;
169181
#endif
170-
}
171-
finally
182+
}
183+
184+
// In ASP.NET Core, you can no longer call `.ToString` on `IHtmlString`
185+
private static string RenderToString(IHtmlString source)
186+
{
187+
#if LEGACYASPNET
188+
return source.ToString();
189+
#else
190+
using (var writer = new StringWriter())
172191
{
173-
Environment.ReturnEngineToPool();
192+
source.WriteTo(writer, HtmlEncoder.Default);
193+
return writer.ToString();
174194
}
195+
#endif
175196
}
176197
}
177198
}

src/React.Core/IReactEnvironment.cs

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/*
1+
/*
22
* Copyright (c) 2014-Present, Facebook, Inc.
33
* All rights reserved.
44
*
@@ -7,7 +7,6 @@
77
* of patent rights can be found in the PATENTS file in the same directory.
88
*/
99

10-
using System;
1110

1211
namespace React
1312
{
@@ -110,5 +109,10 @@ public interface IReactEnvironment
110109
/// Returns the currently held JS engine to the pool. (no-op if engine pooling is disabled)
111110
/// </summary>
112111
void ReturnEngineToPool();
112+
113+
/// <summary>
114+
/// Gets the site-wide configuration.
115+
/// </summary>
116+
IReactSiteConfiguration Configuration { get; }
113117
}
114118
}

src/React.Core/IReactSiteConfiguration.cs

+15-1
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
*/
99

1010
using System;
11-
using Newtonsoft.Json;
1211
using System.Collections.Generic;
12+
using Newtonsoft.Json;
1313

1414
namespace React
1515
{
@@ -193,5 +193,19 @@ public interface IReactSiteConfiguration
193193
/// <param name="handler"></param>
194194
/// <returns></returns>
195195
IReactSiteConfiguration SetExceptionHandler(Action<Exception, string, string> handler);
196+
197+
/// <summary>
198+
/// A provider that returns a nonce to be used on any script tags on the page.
199+
/// This value must match the nonce used in the Content Security Policy header on the response.
200+
/// </summary>
201+
Func<string> ScriptNonceProvider { get; set; }
202+
203+
/// <summary>
204+
/// Sets a provider that returns a nonce to be used on any script tags on the page.
205+
/// This value must match the nonce used in the Content Security Policy header on the response.
206+
/// </summary>
207+
/// <param name="provider"></param>
208+
/// <returns></returns>
209+
IReactSiteConfiguration SetScriptNonceProvider(Func<string> provider);
196210
}
197211
}

src/React.Core/ReactSiteConfiguration.cs

+18
Original file line numberDiff line numberDiff line change
@@ -326,5 +326,23 @@ public IReactSiteConfiguration SetExceptionHandler(Action<Exception, string, str
326326
ExceptionHandler = handler;
327327
return this;
328328
}
329+
330+
/// <summary>
331+
/// A provider that returns a nonce to be used on any script tags on the page.
332+
/// This value must match the nonce used in the Content Security Policy header on the response.
333+
/// </summary>
334+
public Func<string> ScriptNonceProvider { get; set; }
335+
336+
/// <summary>
337+
/// Sets a provider that returns a nonce to be used on any script tags on the page.
338+
/// This value must match the nonce used in the Content Security Policy header on the response.
339+
/// </summary>
340+
/// <param name="provider"></param>
341+
/// <returns></returns>
342+
public IReactSiteConfiguration SetScriptNonceProvider(Func<string> provider)
343+
{
344+
ScriptNonceProvider = provider;
345+
return this;
346+
}
329347
}
330348
}

src/React.Sample.Mvc4/Content/Sample.jsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/**
1+
/**
22
* Copyright (c) 2014-Present, Facebook, Inc.
33
* All rights reserved.
44
*
@@ -9,8 +9,8 @@
99

1010
class CommentsBox extends React.Component {
1111
static propTypes = {
12-
initialComments: React.PropTypes.array.isRequired,
13-
page: React.PropTypes.number
12+
initialComments: PropTypes.array.isRequired,
13+
page: PropTypes.number
1414
};
1515

1616
state = {
@@ -76,7 +76,7 @@ class CommentsBox extends React.Component {
7676

7777
class Comment extends React.Component {
7878
static propTypes = {
79-
author: React.PropTypes.object.isRequired
79+
author: PropTypes.object.isRequired
8080
};
8181

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

9393
class Avatar extends React.Component {
9494
static propTypes = {
95-
author: React.PropTypes.object.isRequired
95+
author: PropTypes.object.isRequired
9696
};
9797

9898
render() {

tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs

+60-2
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
* of patent rights can be found in the PATENTS file in the same directory.
88
*/
99

10+
using System;
11+
using System.Security.Cryptography;
1012
using Moq;
11-
using Xunit;
1213
using React.Web.Mvc;
14+
using Xunit;
1315

1416
namespace React.Tests.Mvc
1517
{
@@ -20,9 +22,10 @@ public class HtmlHelperExtensionsTests
2022
/// This is only required because <see cref="HtmlHelperExtensions"/> can not be
2123
/// injected :(
2224
/// </summary>
23-
private Mock<IReactEnvironment> ConfigureMockEnvironment()
25+
private Mock<IReactEnvironment> ConfigureMockEnvironment(IReactSiteConfiguration configuration = null)
2426
{
2527
var environment = new Mock<IReactEnvironment>();
28+
environment.Setup(x => x.Configuration).Returns(configuration ?? new ReactSiteConfiguration());
2629
AssemblyRegistration.Container.Register(environment.Object);
2730
return environment;
2831
}
@@ -54,6 +57,61 @@ public void ReactWithInitShouldReturnHtmlAndScript()
5457
);
5558
}
5659

60+
[Fact]
61+
public void ScriptNonceIsReturned()
62+
{
63+
string nonce;
64+
using (var random = new RNGCryptoServiceProvider())
65+
{
66+
byte[] nonceBytes = new byte[16];
67+
random.GetBytes(nonceBytes);
68+
nonce = Convert.ToBase64String(nonceBytes);
69+
}
70+
71+
var component = new Mock<IReactComponent>();
72+
component.Setup(x => x.RenderHtml(false, false, null)).Returns("HTML");
73+
component.Setup(x => x.RenderJavaScript()).Returns("JS");
74+
75+
var config = new Mock<IReactSiteConfiguration>();
76+
77+
var environment = ConfigureMockEnvironment(config.Object);
78+
79+
environment.Setup(x => x.Configuration).Returns(config.Object);
80+
environment.Setup(x => x.CreateComponent(
81+
"ComponentName",
82+
new { },
83+
null,
84+
false,
85+
false
86+
)).Returns(component.Object);
87+
88+
// without nonce
89+
var result = HtmlHelperExtensions.ReactWithInit(
90+
htmlHelper: null,
91+
componentName: "ComponentName",
92+
props: new { },
93+
htmlTag: "span"
94+
);
95+
Assert.Equal(
96+
"HTML" + System.Environment.NewLine + "<script>JS</script>",
97+
result.ToString()
98+
);
99+
100+
config.Setup(x => x.ScriptNonceProvider).Returns(() => nonce);
101+
102+
// with nonce
103+
result = HtmlHelperExtensions.ReactWithInit(
104+
htmlHelper: null,
105+
componentName: "ComponentName",
106+
props: new { },
107+
htmlTag: "span"
108+
);
109+
Assert.Equal(
110+
"HTML" + System.Environment.NewLine + "<script nonce=\"" + nonce + "\">JS</script>",
111+
result.ToString()
112+
);
113+
}
114+
57115
[Fact]
58116
public void EngineIsReturnedToPoolAfterRender()
59117
{

0 commit comments

Comments
 (0)