Skip to content

Commit c215112

Browse files
Merge pull request #296 from GeekInTheNorth/release/v3.1.0
Release v3.1.0
2 parents 8497420 + a0d4a98 commit c215112

25 files changed

Lines changed: 416 additions & 193 deletions

src/Stott.Security.Optimizely.Test/Features/Csp/CspOptimizerTests.cs

Lines changed: 224 additions & 91 deletions
Large diffs are not rendered by default.

src/Stott.Security.Optimizely.Test/Features/Csp/CspServiceTests.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,9 @@ public sealed class CspServiceTests
3131

3232
private Mock<IContentSecurityPolicyPage> _mockPage;
3333

34-
private Mock<ICspReportUrlResolver> _mockReportUrlResolver;
35-
3634
[SetUp]
3735
public void SetUp()
3836
{
39-
_mockReportUrlResolver = new Mock<ICspReportUrlResolver>();
40-
_mockReportUrlResolver.Setup(x => x.GetReportToPath()).Returns("https://www.example.com/");
41-
4237
_mockSettingsService = new Mock<ICspSettingsService>();
4338

4439
_mockSandboxService = new Mock<ICspSandboxService>();
@@ -50,8 +45,7 @@ public void SetUp()
5045
_cspService = new CspService(
5146
_mockSettingsService.Object,
5247
_mockPermissionService.Object,
53-
_mockSandboxService.Object,
54-
_mockReportUrlResolver.Object);
48+
_mockSandboxService.Object);
5549
}
5650

5751
[Test]

src/Stott.Security.Optimizely.Test/Features/Csp/Nonce/DefaultNonceProviderTests.cs

Lines changed: 14 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
using System.Collections.Generic;
2-
3-
using EPiServer.Core;
4-
using EPiServer.Web.Templating;
1+
using EPiServer.Core;
2+
using EPiServer.Web.Routing;
53

64
using Microsoft.AspNetCore.Http;
75

@@ -22,6 +20,8 @@ public sealed class DefaultNonceProviderTests
2220

2321
private Mock<ICspSettingsService> _mockCspSettingsService;
2422

23+
private Mock<IPageRouteHelper> _mockPageRouteHelper;
24+
2525
private Mock<HttpContext> _mockContext;
2626

2727
private Mock<HttpRequest> _mockHttpRequest;
@@ -37,6 +37,8 @@ public void SetUp()
3737
_mockHttpContextAccessor = new Mock<IHttpContextAccessor>();
3838
_mockHttpContextAccessor.Setup(x => x.HttpContext).Returns(_mockContext.Object);
3939

40+
_mockPageRouteHelper = new Mock<IPageRouteHelper>();
41+
4042
_mockCspSettingsService = new Mock<ICspSettingsService>();
4143
}
4244

@@ -48,8 +50,9 @@ public void GetNonce_ReturnsNullWhenCspOrNonceIsDisabled(bool isEnabled, bool is
4850
{
4951
// Assert
5052
_mockCspSettingsService.Setup(x => x.Get()).Returns(new CspSettings { IsEnabled = isEnabled, IsNonceEnabled = isNonceEnabled });
53+
_mockPageRouteHelper.Setup(x => x.PageLink).Returns(new PageReference(1));
5154

52-
var nonceProvider = new DefaultNonceProvider(_mockCspSettingsService.Object, _mockHttpContextAccessor.Object);
55+
var nonceProvider = new DefaultNonceProvider(_mockCspSettingsService.Object, _mockHttpContextAccessor.Object, _mockPageRouteHelper.Object);
5356

5457
// Act
5558
var nonce = nonceProvider.GetNonce();
@@ -71,7 +74,7 @@ public void GetNonce_ReturnsNullOnNonContentPathExceptForTheCompiledHeadersPath(
7174

7275
_mockHttpRequest.Setup(x => x.Path).Returns(new PathString(pathValue));
7376

74-
var nonceProvider = new DefaultNonceProvider(_mockCspSettingsService.Object, _mockHttpContextAccessor.Object);
77+
var nonceProvider = new DefaultNonceProvider(_mockCspSettingsService.Object, _mockHttpContextAccessor.Object, _mockPageRouteHelper.Object);
7578

7679
// Act
7780
var nonce = nonceProvider.GetNonce();
@@ -82,20 +85,13 @@ public void GetNonce_ReturnsNullOnNonContentPathExceptForTheCompiledHeadersPath(
8285
}
8386

8487
[Test]
85-
public void GetNonce_ReturnsNullIfRederingContextDoesNotContainContentData()
88+
public void GetNonce_ReturnsNullIfNotRenderingAPage()
8689
{
8790
// Assert
88-
var renderingContext = new ContentRenderingContext(null, (IContentData)null);
89-
91+
_mockPageRouteHelper.Setup(x => x.PageLink).Returns((PageReference)null);
9092
_mockCspSettingsService.Setup(x => x.Get()).Returns(new CspSettings { IsEnabled = true, IsNonceEnabled = true });
9193

92-
_mockContext.Setup(x => x.Items)
93-
.Returns(new Dictionary<object, object>
94-
{
95-
{ ContentRenderingContext.ContentRenderingContextKey, renderingContext }
96-
});
97-
98-
var nonceProvider = new DefaultNonceProvider(_mockCspSettingsService.Object, _mockHttpContextAccessor.Object);
94+
var nonceProvider = new DefaultNonceProvider(_mockCspSettingsService.Object, _mockHttpContextAccessor.Object, _mockPageRouteHelper.Object);
9995

10096
// Act
10197
var nonce = nonceProvider.GetNonce();
@@ -108,20 +104,10 @@ public void GetNonce_ReturnsNullIfRederingContextDoesNotContainContentData()
108104
public void GetNonce_ReturnsNonceIfRederingContextDoesContainContentData()
109105
{
110106
// Assert
111-
var mockContent = new Mock<IContent>();
112-
mockContent.Setup(x => x.ContentLink).Returns(new ContentReference(1));
113-
114-
var renderingContext = new ContentRenderingContext(null, mockContent.Object);
115-
107+
_mockPageRouteHelper.Setup(x => x.PageLink).Returns(new PageReference(1));
116108
_mockCspSettingsService.Setup(x => x.Get()).Returns(new CspSettings { IsEnabled = true, IsNonceEnabled = true });
117109

118-
_mockContext.Setup(x => x.Items)
119-
.Returns(new Dictionary<object, object>
120-
{
121-
{ ContentRenderingContext.ContentRenderingContextKey, renderingContext }
122-
});
123-
124-
var nonceProvider = new DefaultNonceProvider(_mockCspSettingsService.Object, _mockHttpContextAccessor.Object);
110+
var nonceProvider = new DefaultNonceProvider(_mockCspSettingsService.Object, _mockHttpContextAccessor.Object, _mockPageRouteHelper.Object);
125111

126112
// Act
127113
var nonce = nonceProvider.GetNonce();

src/Stott.Security.Optimizely.Test/Features/Csp/Permissions/Save/SavePermissionModelTestCases.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,10 @@ public static IEnumerable<TestCaseData> GetValidUrlTestCases
5959
yield return new TestCaseData("http://www.example.com:80");
6060
yield return new TestCaseData("http://www.example.com/");
6161
yield return new TestCaseData("http://www.example.com/something");
62-
yield return new TestCaseData("http://www.example.com/something");
6362
yield return new TestCaseData("https://www.example.com");
63+
yield return new TestCaseData("https://www.EXAMPLE.com");
6464
yield return new TestCaseData("https://www.example.com/");
6565
yield return new TestCaseData("https://www.example.com/something");
66-
yield return new TestCaseData("https://www.example.com/something");
6766
yield return new TestCaseData("*.mailsite.com");
6867
yield return new TestCaseData("https://onlinebanking.jumbobank.com");
6968
yield return new TestCaseData("media1.com");
@@ -78,6 +77,12 @@ public static IEnumerable<TestCaseData> GetValidUrlTestCases
7877
yield return new TestCaseData("http://www.example.io");
7978
yield return new TestCaseData("https://www.example.co");
8079
yield return new TestCaseData("https://www.example.io");
80+
yield return new TestCaseData("https://abc1d23456de7f890g12-h34ijklm567nop890qr12stu3v4567wx.ssl.cf5.rackcdn.com/1234/v4.3.2iframeResizer.min.js");
81+
yield return new TestCaseData("https://example.com/xyZxYzabcDEFghij-eu1/zaius-min.js");
82+
yield return new TestCaseData("https://example.com/Test@test");
83+
yield return new TestCaseData("x.com");
84+
yield return new TestCaseData("https://x.com");
85+
yield return new TestCaseData("https://*");
8186
}
8287
}
8388

@@ -126,7 +131,6 @@ public static IEnumerable<TestCaseData> GetValidDirectivesTestCases
126131
yield return new TestCaseData(new List<string> { CspConstants.Directives.ManifestSource });
127132
yield return new TestCaseData(new List<string> { CspConstants.Directives.MediaSource });
128133
yield return new TestCaseData(new List<string> { CspConstants.Directives.ObjectSource });
129-
yield return new TestCaseData(new List<string> { CspConstants.Directives.PreFetchSource });
130134
yield return new TestCaseData(new List<string> { CspConstants.Directives.ScriptSourceAttribute });
131135
yield return new TestCaseData(new List<string> { CspConstants.Directives.ScriptSourceElement });
132136
yield return new TestCaseData(new List<string> { CspConstants.Directives.ScriptSource });
@@ -135,7 +139,7 @@ public static IEnumerable<TestCaseData> GetValidDirectivesTestCases
135139
yield return new TestCaseData(new List<string> { CspConstants.Directives.StyleSource });
136140
yield return new TestCaseData(new List<string> { CspConstants.Directives.WorkerSource });
137141
yield return new TestCaseData(new List<string> { CspConstants.Directives.BaseUri, CspConstants.Directives.ObjectSource });
138-
yield return new TestCaseData(new List<string> { CspConstants.Directives.ChildSource, CspConstants.Directives.PreFetchSource });
142+
yield return new TestCaseData(new List<string> { CspConstants.Directives.ChildSource, CspConstants.Directives.FrameSource });
139143
yield return new TestCaseData(new List<string> { CspConstants.Directives.FontSource, CspConstants.Directives.ScriptSourceAttribute });
140144
yield return new TestCaseData(new List<string> { CspConstants.Directives.FontSource, CspConstants.Directives.ScriptSourceElement });
141145
yield return new TestCaseData(new List<string> { CspConstants.Directives.FrameAncestors, CspConstants.Directives.ScriptSource });

src/Stott.Security.Optimizely.Test/Features/Csp/Reporting/ViolationReportSummaryTestCases.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ public static IEnumerable<TestCaseData> DirectiveSuggestionTestCases
8888
yield return new TestCaseData(CspConstants.Directives.ManifestSource, new List<string> { CspConstants.Directives.ManifestSource });
8989
yield return new TestCaseData(CspConstants.Directives.MediaSource, new List<string> { CspConstants.Directives.MediaSource });
9090
yield return new TestCaseData(CspConstants.Directives.ObjectSource, new List<string> { CspConstants.Directives.ObjectSource });
91-
yield return new TestCaseData(CspConstants.Directives.PreFetchSource, new List<string> { CspConstants.Directives.PreFetchSource });
9291
yield return new TestCaseData(CspConstants.Directives.ScriptSourceAttribute, new List<string> { CspConstants.Directives.ScriptSourceAttribute, CspConstants.Directives.ScriptSource });
9392
yield return new TestCaseData(CspConstants.Directives.ScriptSourceElement, new List<string> { CspConstants.Directives.ScriptSourceElement, CspConstants.Directives.ScriptSource });
9493
yield return new TestCaseData(CspConstants.Directives.ScriptSource, new List<string> { CspConstants.Directives.ScriptSource });

src/Stott.Security.Optimizely.Test/Features/Csp/Reporting/ViolationReportSummaryTests.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
using Stott.Security.Optimizely.Features.Csp.Reporting;
1010

1111
[TestFixture]
12-
public class ViolationReportSummaryTests
12+
public sealed class ViolationReportSummaryTests
1313
{
1414
[Test]
1515
[TestCaseSource(typeof(ViolationReportSummaryTestCases), nameof(ViolationReportSummaryTestCases.SanitizedSourceTestCases))]
@@ -18,7 +18,7 @@ public void SanitizedSourceReturnsEitherTheFullDomainOrOriginalString(
1818
string expectedSanitizedSource)
1919
{
2020
// Arrange
21-
var summary = new ViolationReportSummary(1, source, string.Empty, 1, DateTime.Now);
21+
var summary = new ViolationReportSummary(Guid.NewGuid(), source, string.Empty, 1, DateTime.Now);
2222

2323
// Assert
2424
Assert.That(summary.SanitizedSource, Is.EqualTo(expectedSanitizedSource));
@@ -29,7 +29,7 @@ public void SanitizedSourceReturnsEitherTheFullDomainOrOriginalString(
2929
public void CreatesAppropriateSuggestionsForWildCardDomains(string source, IList<string> expectedSuggestions)
3030
{
3131
// Arrange
32-
var summary = new ViolationReportSummary(1, source, string.Empty, 1, DateTime.Now);
32+
var summary = new ViolationReportSummary(Guid.NewGuid(), source, string.Empty, 1, DateTime.Now);
3333

3434
// Assert
3535
Assert.That(summary.SourceSuggestions, Is.EquivalentTo(expectedSuggestions));
@@ -40,7 +40,7 @@ public void CreatesAppropriateSuggestionsForWildCardDomains(string source, IList
4040
public void CreatesASingleSuggestionMatchingTheSourceWhenSourceIsNotAUrl(string source)
4141
{
4242
// Arrange
43-
var summary = new ViolationReportSummary(1, source, string.Empty, 1, DateTime.Now);
43+
var summary = new ViolationReportSummary(Guid.NewGuid(), source, string.Empty, 1, DateTime.Now);
4444

4545
// Assert
4646
Assert.Multiple(() =>
@@ -55,7 +55,7 @@ public void CreatesASingleSuggestionMatchingTheSourceWhenSourceIsNotAUrl(string
5555
public void CreatesAppropriateSuggestionsForDirectives(string directive, IList<string> expectedDirectives)
5656
{
5757
// Arrange
58-
var summary = new ViolationReportSummary(1, string.Empty, directive, 1, DateTime.Now);
58+
var summary = new ViolationReportSummary(Guid.NewGuid(), string.Empty, directive, 1, DateTime.Now);
5959

6060
// Assert
6161
Assert.That(summary.DirectiveSuggestions, Is.EquivalentTo(expectedDirectives));

src/Stott.Security.Optimizely.Test/Features/Header/HeaderCompilationServiceTests.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,4 +171,50 @@ public async Task GetSecurityHeadersAsync_UsesPageSpecificCacheKeyWhenPageDataIs
171171
Assert.That(cacheKeyUsed, Is.Not.EqualTo(CspConstants.CacheKeys.CompiledHeaders));
172172
Assert.That(cacheKeyUsed.Contains(CspConstants.CacheKeys.CompiledHeaders), Is.True);
173173
}
174+
175+
[Test]
176+
public async Task GetSecurityHeadersAsync_GivenReportingEndpointHeaderIsPresent_ThenInternalReportingPlaceholderIsUpdated()
177+
{
178+
// Arrange
179+
var headers = new List<HeaderDto>
180+
{
181+
new() { Key = CspConstants.HeaderNames.ContentSecurityPolicy, Value = "default-src 'self'; report-to /report" },
182+
new() { Key = CspConstants.HeaderNames.ReportingEndpoints, Value = $"stott-security-endpoint=\"{CspConstants.InternalReportingPlaceholder}\""}
183+
};
184+
185+
_cacheWrapper.Setup(x => x.Get<List<HeaderDto>>(It.IsAny<string>()))
186+
.Returns(headers);
187+
188+
_mockReportUrlResolver.Setup(x => x.GetReportToPath()).Returns("https://example.com/report");
189+
190+
// Act
191+
var result = await _service.GetSecurityHeadersAsync(null);
192+
var reportingHeader = result.Find(x => x.Key == CspConstants.HeaderNames.ReportingEndpoints);
193+
194+
// Assert
195+
Assert.That(reportingHeader?.Value, Is.EqualTo("stott-security-endpoint=\"https://example.com/report\""));
196+
}
197+
198+
[Test]
199+
public async Task GetSecurityHeadersAsync_GivenReportingEndpointHeaderIsPresent_AndInternalAndExternalReportingIsEnabled_ThenInternalReportingPlaceholderIsUpdated()
200+
{
201+
// Arrange
202+
var headers = new List<HeaderDto>
203+
{
204+
new() { Key = CspConstants.HeaderNames.ContentSecurityPolicy, Value = "default-src 'self'; report-to /report" },
205+
new() { Key = CspConstants.HeaderNames.ReportingEndpoints, Value = $"stott-security-endpoint=\"{CspConstants.InternalReportingPlaceholder}\", stott-security-external-endpoint=\"https://www.external.com/report/\""}
206+
};
207+
208+
_cacheWrapper.Setup(x => x.Get<List<HeaderDto>>(It.IsAny<string>()))
209+
.Returns(headers);
210+
211+
_mockReportUrlResolver.Setup(x => x.GetReportToPath()).Returns("https://example.com/report");
212+
213+
// Act
214+
var result = await _service.GetSecurityHeadersAsync(null);
215+
var reportingHeader = result.Find(x => x.Key == CspConstants.HeaderNames.ReportingEndpoints);
216+
217+
// Assert
218+
Assert.That(reportingHeader?.Value, Is.EqualTo("stott-security-endpoint=\"https://example.com/report\", stott-security-external-endpoint=\"https://www.external.com/report/\""));
219+
}
174220
}

src/Stott.Security.Optimizely.Test/Features/Pages/PageCspSourceMappingValidationTests.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,29 @@ public void ShouldValidateQuotedSources(string source, bool shouldError)
5858
}
5959

6060
[Test]
61-
[TestCaseSource(typeof(SavePermissionModelTestCases), nameof(SavePermissionModelTestCases.GetValidUrlTestCases))]
61+
[TestCase("ws://www.example.com")]
62+
[TestCase("wss://www.example.com")]
63+
[TestCase("http://www.example.com")]
64+
[TestCase("http://www.example.com:80")]
65+
[TestCase("http://www.example.com/")]
66+
[TestCase("http://www.example.com/something")]
67+
[TestCase("https://www.example.com")]
68+
[TestCase("https://www.example.com/")]
69+
[TestCase("https://www.example.com/something")]
70+
[TestCase("*.mailsite.com")]
71+
[TestCase("https://onlinebanking.jumbobank.com")]
72+
[TestCase("media1.com")]
73+
[TestCase("*.trusted.com")]
74+
[TestCase("wss://localhost:44323")]
75+
[TestCase("https://localhost:*")]
76+
[TestCase("*.example.co")]
77+
[TestCase("*.example.io")]
78+
[TestCase("http://*.example.co")]
79+
[TestCase("http://*.example.io")]
80+
[TestCase("http://www.example.co")]
81+
[TestCase("http://www.example.io")]
82+
[TestCase("https://www.example.co")]
83+
[TestCase("https://www.example.io")]
6284
public void ShouldValidateUrlsWithOrWithoutWildcards(string source)
6385
{
6486
// Arrange

src/Stott.Security.Optimizely/Common/CspConstants.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ public static class CspConstants
1414

1515
public const string NoncePlaceholder = "##NONCE##";
1616

17+
public const string InternalReportingPlaceholder = "##INTERNAL_REPORTING##";
18+
1719
public const string StrictDynamic = "'strict-dynamic'";
1820

1921
public static int LogRetentionDays => 30;
2022

2123
public const int TwoYearsInSeconds = 63072000;
2224

23-
public const int SplitThreshold = 8000;
25+
public const int SplitThreshold = 8100;
2426

2527
public const int SimplifyThreshold = 12000;
2628

@@ -43,7 +45,6 @@ public static class CspConstants
4345
Directives.ManifestSource,
4446
Directives.MediaSource,
4547
Directives.ObjectSource,
46-
Directives.PreFetchSource,
4748
Directives.ScriptSourceAttribute,
4849
Directives.ScriptSourceElement,
4950
Directives.ScriptSource,
@@ -141,8 +142,6 @@ public static class Directives
141142

142143
public const string ObjectSource = "object-src";
143144

144-
public const string PreFetchSource = "prefetch-src";
145-
146145
public const string Sandbox = "sandbox";
147146

148147
public const string ScriptSourceAttribute = "script-src-attr";
@@ -196,7 +195,7 @@ public static class CacheKeys
196195

197196
public static class RegexPatterns
198197
{
199-
public const string UrlDomain = "^([a-z0-9\\/\\-\\._\\:\\*\\[\\]\\@]{3,}\\.{1}[a-z0-9\\/\\-\\._\\:\\*\\[\\]\\@]{2,})$";
198+
public const string UrlDomain = @"^(?:[a-zA-Z][a-zA-Z0-9+\-.]*:\/\/)?(?:\*\.[a-zA-Z0-9\-]+(?:\.[a-zA-Z0-9\-]+)+|[a-zA-Z0-9\-]+(?:\.[a-zA-Z0-9\-]+)+|\*)(?::\d+|:\*)?(?:\/[^\s]*)?$";
200199

201200
public const string UrlLocalHost = "^([a-z]{2,5}\\:{1}\\/\\/localhost\\:([0-9]{1,5}|\\*{1}))$";
202201
}

0 commit comments

Comments
 (0)