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 a nonce option and Render testing #465

Merged
merged 3 commits into from
Apr 4, 2020
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion docs/AspDotNetCore.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF
```html
<mini-profiler />
```
<sub>Note: `<mini-profiler>` has many options like `max-traces`, `position`, etc. [You can find them in code here](https://github.com/MiniProfiler/dotnet/blob/master/src/MiniProfiler.AspNetCore.Mvc/MiniProfilerScriptTagHelper.cs).</sub>
<sub>Note: `<mini-profiler>` has many options like `max-traces`, `position`, `color-scheme`, `nonce`, etc. [You can find them in code here](https://github.com/MiniProfiler/dotnet/blob/master/src/MiniProfiler.AspNetCore.Mvc/MiniProfilerScriptTagHelper.cs).</sub>
<sub>Note #2: The above tag helper registration may go away in future versions of ASP.NET Core, they're working on smoother alternatives here.</sub>


Expand Down
12 changes: 12 additions & 0 deletions docs/Releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ layout: "default"
### Release Notes
This page tracks major changes included in any update starting with version 4.0.0.3

#### Version 4.2.0 (In preview)
- Added `<script nonce="..." />` to rendering for CSP support ([#465](https://github.com/MiniProfiler/dotnet/pull/465))
- Added dark and "auto" (system preference decides) color themes, total is "Light", "Dark", and "Auto" ([#451](https://github.com/MiniProfiler/dotnet/pull/451))
- Generally moves to CSS 3 variables, for easier custom themes as well.
- Added `SqlServerFormatter.IncludeParameterValues` for excluding actual values in output if desired ([#463](https://github.com/MiniProfiler/dotnet/pull/463))
- Fix for ['i.Started.toUTCString is not a function'](https://github.com/MiniProfiler/dotnet/pull/462) when global serializer options are changed.
- Removed jQuery (built-in) dependency ([#442](https://github.com/MiniProfiler/dotnet/pull/442))
- Drops IE 11 support
- Fix for missing `IMemoryCache` depending on config ([#440](https://github.com/MiniProfiler/dotnet/pull/440))
- Updates `MySqlConnector` to 0.60.1 for misc fixes ([#432](https://github.com/MiniProfiler/dotnet/pull/432)) (thanks [@bgrainger](https://github.com/bgrainger)!)


#### Version 4.1.0
- ASP.NET Core 3.0 support ([MiniProfiler.AspNetCore](https://www.nuget.org/packages/MiniProfiler.AspNetCore/) and [MiniProfiler.AspNetCore.Mvc](https://www.nuget.org/packages/MiniProfiler.AspNetCore.Mvc/) packages, now with a `netcoreapp3.0` build)
- Error support via `CustomTiming.Errored = true`, this will turn the UI red to raise error awareness ([#418](https://github.com/MiniProfiler/dotnet/pull/418) & [#420](https://github.com/MiniProfiler/dotnet/pull/420))
Expand Down
2 changes: 1 addition & 1 deletion samples/Samples.AspNetCore3/Views/Shared/_Layout.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
@RenderSection("scripts", required: false)

@* Simple options are exposed...or make a full options class for customizing. *@
<mini-profiler position="@RenderPosition.Right" max-traces="5" color-scheme="ColorScheme.Auto" />
<mini-profiler position="@RenderPosition.Right" max-traces="5" color-scheme="ColorScheme.Auto" nonce="45" />
Copy link
Contributor

Choose a reason for hiding this comment

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

Given that nonce is supposed to be random on every page load, I'm not sure it makes sense to have it as an attribute on mini-profiler? Would the intention instead be to use something like @NonceSource.GetNonce()?

As far as documentation / samples go, what is the story for "I want to inform miniprofiler about a nonce that comes from some piece of middleware?" The middleware shoves it in the context somewhere and it gets pulled in here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Correct - you'd pass in a unique value. Even putting say a <Func<HttpContext, string> is complicated due to the different primitives with .NET vs. .NET Core...maybe when we can drop .NET Full Framework support one day that gets easier since there's no common ancestor need.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh sorry misread the second part - there's no built-in middleware story I can think of that makes sense here. The problem is generally you want MiniProfiler to profile the things you're doing and we have an order inversion problems with dependencies about the middleware doing it. Either direction we choose would be wrong for some subset of users. At least in v1, I'd want to leave it up to the users to handle their nonce generation.

@*<mini-profiler options="new RenderOptions { Position = RenderPosition.Right, MaxTracesToShow = 5, ColorScheme = ColorScheme.Auto }" />*@
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ public class MiniProfilerScriptTagHelper : TagHelper
[HtmlAttributeName("color-scheme")]
public ColorScheme? ColorScheme { get; set; }

/// <summary>
/// The JavaScript nonce (if any) to use on this script tag render.
/// </summary>
[HtmlAttributeName("nonce")]
public string Nonce { get; set; }

/// <summary>
/// The options to use when rendering this MiniProfiler.
/// Note: overrides all other options.
Expand All @@ -77,6 +83,7 @@ private RenderOptions GetOptions()
if (ShowControls.HasValue) options.ShowControls = ShowControls;
if (StartHidden.HasValue) options.StartHidden = StartHidden;
if (ColorScheme.HasValue) options.ColorScheme = ColorScheme;
if (Nonce.HasValue()) options.Nonce = Nonce;

return options;
}
Expand Down
6 changes: 5 additions & 1 deletion src/MiniProfiler.Shared/Internal/Render.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public static string Includes(
var sb = StringBuilderCache.Get();
var options = profiler.Options;

sb.Append("<script async=\"async\" id=\"mini-profiler\" src=\"");
sb.Append("<script async id=\"mini-profiler\" src=\"");
sb.Append(path);
sb.Append("includes.min.js?v=");
sb.Append(options.VersionHash);
Expand Down Expand Up @@ -82,6 +82,10 @@ public static string Includes(
{
sb.Append(" data-start-hidden=\"true\"");
}
if (renderOptions?.Nonce.HasValue() ?? false)
{
sb.Append(" nonce=\"").Append(renderOptions.Nonce).Append("\"");
Copy link
Contributor

@vcsjones vcsjones Apr 4, 2020

Choose a reason for hiding this comment

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

Since I see that sb contains other markup elements, (line 114) it is presumed that it's contents are HtmlSafe? Is there any concern that Nonce is an XSS point, or at least requires some escaping?

Alternatively, what about simply rendering the incorrect thing? If a nonce is supposed to be random and someone's nonce generator uses the printable ascii range (0x20-0x7F), &, ", etc. could feasibly end up breaking rendering if this isn't escaping like I am concerned about.

Is it assumed that the caller will do the escaping for miniprofiler?

Copy link
Member Author

Choose a reason for hiding this comment

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

Doh, absolutely - this was the first text thing a user can pass and definitely needs encoding, total goof. Updated (and added a few tests for these cases).

}

sb.Append(" data-max-traces=\"");
sb.Append((renderOptions?.MaxTracesToShow ?? options.PopupMaxTracesToShow).ToString(CultureInfo.InvariantCulture));
Expand Down
6 changes: 6 additions & 0 deletions src/MiniProfiler.Shared/RenderOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,11 @@ public class RenderOptions
/// Defaults to <see cref="MiniProfilerBaseOptions.ColorScheme"/>.
/// </summary>
public ColorScheme? ColorScheme { get; set; }

/// <summary>
/// A one-time-use nonce to render in the script tag.
/// </summary>
/// <remarks>https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script</remarks>
public string Nonce { get; set; }
}
}
108 changes: 108 additions & 0 deletions tests/MiniProfiler.Tests/RenderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using System;
using System.Collections.Generic;
using StackExchange.Profiling.Internal;
using Xunit;
using Xunit.Abstractions;

namespace StackExchange.Profiling.Tests
{
public class RenderTests : BaseTest
{
public RenderTests(ITestOutputHelper output) : base(output) { }

[Fact]
public void DefaultRender()
{
var profiler = GetBasicProfiler();
var renderOptions = new RenderOptions();
var result = Render.Includes(profiler, "/", true, renderOptions, new List<Guid>() { profiler.Id });
Output.WriteLine("Result: " + result);

Assert.NotNull(result);
Assert.Contains("id=\"mini-profiler\"", result);

var expected = $@"<script async id=""mini-profiler"" src=""/includes.min.js?v={Options.VersionHash}"" data-version=""{Options.VersionHash}"" data-path=""/"" data-current-id=""{profiler.Id}"" data-ids=""{profiler.Id}"" data-position=""Left"""" data-scheme=""Light"" data-authorized=""true"" data-max-traces=""15"" data-toggle-shortcut=""Alt+P"" data-trivial-milliseconds=""2.0"" data-ignored-duplicate-execute-types=""Open,OpenAsync,Close,CloseAsync""></script>";
Assert.Equal(expected, result);
}

[Fact]
public void OptionsSet()
{
var profiler = GetBasicProfiler();
var renderOptions = new RenderOptions()
{
ColorScheme = ColorScheme.Auto,
MaxTracesToShow = 12,
Nonce = "myNonce",
PopupToggleKeyboardShortcut = "Alt+Q",
Position = RenderPosition.Right,
ShowControls = true,
ShowTimeWithChildren = true,
ShowTrivial = true,
StartHidden = true,
TrivialDurationThresholdMilliseconds = 23
};
var result = Render.Includes(profiler, "/", true, renderOptions, new List<Guid>() { profiler.Id });
Output.WriteLine("Result: " + result);

Assert.NotNull(result);
Assert.Contains("id=\"mini-profiler\"", result);

var expected = $@"<script async id=""mini-profiler"" src=""/includes.min.js?v={Options.VersionHash}"" data-version=""{Options.VersionHash}"" data-path=""/"" data-current-id=""{profiler.Id}"" data-ids=""{profiler.Id}"" data-position=""Right"""" data-scheme=""Auto"" data-authorized=""true"" data-trivial=""true"" data-children=""true"" data-controls=""true"" data-start-hidden=""true"" nonce=""myNonce"" data-max-traces=""12"" data-toggle-shortcut=""Alt+Q"" data-trivial-milliseconds=""23"" data-ignored-duplicate-execute-types=""Open,OpenAsync,Close,CloseAsync""></script>";
Assert.Equal(expected, result);
}

[Theory]
[InlineData(null, @"data-scheme=""Light""")]
[InlineData(ColorScheme.Auto, @"data-scheme=""Auto""")]
[InlineData(ColorScheme.Dark, @"data-scheme=""Dark""")]
[InlineData(ColorScheme.Light, @"data-scheme=""Light""")]
public void ColorSchemes(ColorScheme? scheme, string expected)
{
var profiler = GetBasicProfiler();
var renderOptions = new RenderOptions() { ColorScheme = scheme };

var result = Render.Includes(profiler, " / ", true, renderOptions);
Output.WriteLine("Result: " + result);

Assert.NotNull(result);
Assert.Contains(expected, result);
}

[Theory]
[InlineData(null, @"data-position=""Left""")]
[InlineData(RenderPosition.BottomLeft, @"data-position=""BottomLeft""")]
[InlineData(RenderPosition.BottomRight, @"data-position=""BottomRight""")]
[InlineData(RenderPosition.Left, @"data-position=""Left""")]
[InlineData(RenderPosition.Right, @"data-position=""Right""")]
public void Positions(RenderPosition? position, string expected)
{
var profiler = GetBasicProfiler();
var renderOptions = new RenderOptions() { Position = position };

var result = Render.Includes(profiler, " / ", true, renderOptions);
Output.WriteLine("Result: " + result);

Assert.NotNull(result);
Assert.Contains(expected, result);
}

[Fact]
public void Nonce()
{
var profiler = GetBasicProfiler();
var renderOptions = new RenderOptions();

// Default
var result = Render.Includes(profiler, "/", true, renderOptions);
Output.WriteLine("Result: " + result);
Assert.DoesNotContain("nonce", result);

// With nonce
var nonce = Guid.NewGuid().ToString();
renderOptions.Nonce = nonce;
result = Render.Includes(profiler, "/", true, renderOptions);
Assert.Contains($@"nonce=""{nonce}""", result);
}
}
}