Skip to content

Commit d7b6a4f

Browse files
Add support for script nonce attributes
1 parent e5cb5d8 commit d7b6a4f

File tree

5 files changed

+129
-29
lines changed

5 files changed

+129
-29
lines changed

src/React.AspNet/HtmlHelperExtensions.cs

+30-24
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,8 +8,6 @@
88
*/
99

1010
using System;
11-
using React.Exceptions;
12-
using React.TinyIoC;
1311

1412
#if LEGACYASPNET
1513
using System.Web;
@@ -129,16 +127,7 @@ public static IHtmlString ReactWithInit<T>(
129127
}
130128
var html = reactComponent.RenderHtml(clientOnly, exceptionHandler: exceptionHandler);
131129

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());
130+
return new HtmlString(html + System.Environment.NewLine + GetScriptTag(reactComponent.RenderJavaScript()).ToString());
142131
}
143132
finally
144133
{
@@ -155,23 +144,40 @@ public static IHtmlString ReactInitJavaScript(this IHtmlHelper htmlHelper, bool
155144
{
156145
try
157146
{
158-
var script = Environment.GetInitJavaScript(clientOnly);
147+
return GetScriptTag(Environment.GetInitJavaScript(clientOnly));
148+
}
149+
finally
150+
{
151+
Environment.ReturnEngineToPool();
152+
}
153+
}
154+
155+
private static IHtmlString GetScriptTag(string script)
156+
{
159157
#if LEGACYASPNET
160-
var tag = new TagBuilder("script")
161-
{
162-
InnerHtml = script
163-
};
164-
return new HtmlString(tag.ToString());
158+
var tag = new TagBuilder("script")
159+
{
160+
InnerHtml = script,
161+
};
162+
163+
if (Environment.Configuration.ScriptNonceProvider != null)
164+
{
165+
string nonce = Environment.Configuration.ScriptNonceProvider();
166+
tag.Attributes.Add("nonce", nonce );
167+
}
168+
169+
return new HtmlString(tag.ToString());
165170
#else
166171
var tag = new TagBuilder("script");
167172
tag.InnerHtml.AppendHtml(script);
168-
return tag;
169-
#endif
170-
}
171-
finally
173+
174+
if (Environment.Configuration.ScriptNonceProvider != null)
172175
{
173-
Environment.ReturnEngineToPool();
176+
tag.Attributes.Add("nonce", Environment.Configuration.ScriptNonceProvider());
174177
}
178+
179+
return tag;
180+
#endif
175181
}
176182
}
177183
}

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
}

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)