-
Notifications
You must be signed in to change notification settings - Fork 10.7k
Make Virtualize component CSP-compliant #66680
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
Merged
ilonatommy
merged 27 commits into
dotnet:main
from
ilonatommy:virtuazliation-does-not-interfere-with-strict-csp
Jun 16, 2026
Merged
Changes from all commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
c8d55df
MutationObserver fix.
ilonatommy d2b52df
Move sample code to e2e test + apply feedback.
ilonatommy f35e724
cleanup
ilonatommy 0af68e2
Apply placeholders style in the same atomic task as spacers styles.
ilonatommy a54ac13
Use CSS custom properties + static stylesheet for Virtualize styles
ilonatommy d7cd8e5
Merge remote-tracking branch 'origin/main' into virtuazliation-does-n…
ilonatommy 58f35f7
Add strict-style-csp E2E test for Virtualize
ilonatommy ec4ec33
Fix tests: apply styles synchonously.
ilonatommy dd53eb0
Merge remote-tracking branch 'origin/main' into virtuazliation-does-n…
ilonatommy a263b72
Merge branch 'main' into virtuazliation-does-not-interfere-with-stric…
ilonatommy e2407e9
Fallback with `none` css can be in JS only.
ilonatommy 3d11c5a
Revert blank line removal.
ilonatommy 50d2980
Test cleanup.
ilonatommy 7d74c8e
Feedback: itemprovider tests.
ilonatommy 6530b3d
Rename to a better scoped string.
ilonatommy 9e1e356
The number of items we're changing styles of is not enough to justify…
ilonatommy 2d42322
Simplify style communication: each operation has a separate custom at…
ilonatommy 6a030f8
Add scrubbing + test.
ilonatommy a83c6db
Fix test.
ilonatommy e237ef9
Use the renderer to serialize the style attribute.
ilonatommy 22450ee
Cleanup.
ilonatommy cc38822
Remove duplicate comment.
ilonatommy 97bd7f0
C# doesn't override the styles anymore, re-applying them in JS is no-…
ilonatommy e48859f
Add test for QuickGrid.
ilonatommy bde7dcf
Limit observed nodes to spacers.
ilonatommy b701652
Using custom properties is not justified anymore.
ilonatommy a54205e
Feedback: correct stale comment.
ilonatommy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
147 changes: 147 additions & 0 deletions
147
src/Components/test/E2ETest/ServerExecutionTests/VirtualizationCspTest.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using BasicTestApp; | ||
| using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; | ||
| using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; | ||
| using Microsoft.AspNetCore.E2ETesting; | ||
| using OpenQA.Selenium; | ||
| using TestServer; | ||
| using Xunit.Abstractions; | ||
|
|
||
| namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests; | ||
|
|
||
| public class VirtualizationCspTest : ServerTestBase<BasicTestAppServerSiteFixture<ServerStartup>> | ||
| { | ||
| public VirtualizationCspTest( | ||
| BrowserFixture browserFixture, | ||
| BasicTestAppServerSiteFixture<ServerStartup> serverFixture, | ||
| ITestOutputHelper output) | ||
| : base(browserFixture, serverFixture, output) | ||
| { | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Virtualize_WithItems_DoesNotViolate_StrictStyleCspPolicy() | ||
| { | ||
| // strict-style-csp causes ServerStartup to add `Content-Security-Policy: style-src 'self'`. | ||
| Navigate($"{ServerPathBase}?strict-style-csp=true"); | ||
| Browser.MountTestComponent<VirtualizationCsp>(); | ||
|
|
||
| var container = Browser.Exists(By.Id("csp-container")); | ||
| Browser.True(() => container.FindElements(By.CssSelector(".csp-item")).Count > 0); | ||
|
|
||
| // Scroll to force spacer style updates which set CSS custom properties via CSSOM. | ||
| var js = (IJavaScriptExecutor)Browser; | ||
| js.ExecuteScript("document.getElementById('csp-container').scrollTop = 5000;"); | ||
|
|
||
| // Allow the MutationObserver microtask + CSSOM updates to flush. | ||
| Browser.True(() => | ||
| { | ||
| var topSpacer = container.FindElements(By.CssSelector(":scope > div"))[0]; | ||
| var height = topSpacer.GetDomAttribute("data-blazor-virtualize-reserved-height"); | ||
| return height != null && height != "0"; | ||
| }); | ||
|
|
||
| AssertNoStyleCspViolations(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Virtualize_WithItemsProvider_DoesNotViolate_StrictStyleCspPolicy() | ||
| { | ||
| // strict-style-csp causes ServerStartup to add `Content-Security-Policy: style-src 'self'`. | ||
| Navigate($"{ServerPathBase}?strict-style-csp=true&mode=items-provider"); | ||
| Browser.MountTestComponent<VirtualizationCsp>(); | ||
|
|
||
| var container = Browser.Exists(By.Id("csp-container-async")); | ||
|
|
||
| // Wait for either placeholders (during in-flight load) or resolved items. | ||
| Browser.True(() => container.FindElements( | ||
| By.CssSelector(".csp-placeholder, .csp-item-async")).Count > 0); | ||
|
|
||
| // Scroll to trigger new placeholder renders and spacer style updates. | ||
| var js = (IJavaScriptExecutor)Browser; | ||
| js.ExecuteScript("document.getElementById('csp-container-async').scrollTop = 5000;"); | ||
|
|
||
| // Wait for resolved items after scroll so the placeholder path has executed. | ||
| Browser.True(() => container.FindElements(By.CssSelector(".csp-item-async")).Count > 0); | ||
|
|
||
| AssertNoStyleCspViolations(); | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData("url(https://example.invalid/x.png)")] // non-numeric, CSS-like | ||
| [InlineData("120px")] // a unit suffix is no longer accepted | ||
| [InlineData("abc")] // non-numeric | ||
| [InlineData("1e9999")] // overflows to Infinity (not finite) | ||
| [InlineData("NaN")] // non-finite | ||
| [InlineData("1 2")] // multi-token | ||
| [InlineData("")] // empty | ||
| public void Virtualize_RejectsUnexpectedLayoutAttributeValues(string invalidValue) | ||
| { | ||
| // Values that don't parse as a finite number must not propagate to CSS custom properties. | ||
| Navigate($"{ServerPathBase}?strict-style-csp=true"); | ||
| Browser.MountTestComponent<VirtualizationCsp>(); | ||
|
|
||
| var container = Browser.Exists(By.Id("csp-container")); | ||
| Browser.True(() => container.FindElements(By.CssSelector(".csp-item")).Count > 0); | ||
|
|
||
| var js = (IJavaScriptExecutor)Browser; | ||
| const string spacerSelector = "#csp-container > div:first-of-type"; | ||
|
|
||
| js.ExecuteScript( | ||
| "var el = document.querySelector(arguments[0]);" + | ||
| "el.setAttribute('data-blazor-virtualize-reserved-height', arguments[1]);" + | ||
| "el.setAttribute('data-blazor-virtualize-loop-breaker-transform', arguments[1]);", | ||
| spacerSelector, invalidValue); | ||
|
|
||
| // Invalid values must result in the inline style being removed (empty). | ||
| Browser.True(() => | ||
| { | ||
| var heightStyle = (string)js.ExecuteScript( | ||
| "return document.querySelector(arguments[0]).style.height;", | ||
| spacerSelector); | ||
| var transformStyle = (string)js.ExecuteScript( | ||
| "return document.querySelector(arguments[0]).style.transform;", | ||
| spacerSelector); | ||
| return heightStyle == "" && transformStyle == ""; | ||
| }); | ||
|
|
||
| AssertNoStyleCspViolations(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void QuickGrid_WithVirtualize_DoesNotViolate_StrictStyleCspPolicy() | ||
| { | ||
| // QuickGrid uses Virtualize internally. Validates the integration is CSP-clean end-to-end. | ||
| Navigate($"{ServerPathBase}?strict-style-csp=true"); | ||
| Browser.MountTestComponent<BasicTestApp.QuickGridTest.QuickGridCsp>(); | ||
|
|
||
| var container = Browser.Exists(By.Id("csp-quickgrid")); | ||
|
|
||
| // Wait for QuickGrid to render at least one data row. | ||
| Browser.True(() => container.FindElements(By.CssSelector("tbody tr")).Count > 0); | ||
|
|
||
| // Scroll to force spacer style updates which set CSS custom properties via CSSOM. | ||
| var js = (IJavaScriptExecutor)Browser; | ||
| js.ExecuteScript("document.getElementById('csp-quickgrid').scrollTop = 20000;"); | ||
|
|
||
| // Wait for the top spacer's reserved-height attribute to grow past 0 after scrolling. | ||
| Browser.True(() => | ||
| { | ||
| var spacers = container.FindElements(By.CssSelector("[data-blazor-virtualize-reserved-height]")); | ||
| return spacers.Count > 0 | ||
| && spacers[0].GetDomAttribute("data-blazor-virtualize-reserved-height") != "0"; | ||
| }); | ||
|
|
||
| AssertNoStyleCspViolations(); | ||
| } | ||
|
|
||
| private void AssertNoStyleCspViolations() | ||
| { | ||
| const string cspErrorMessage = "violates the following Content Security Policy directive: \"style-src"; | ||
| var logs = Browser.Manage().Logs.GetLog(LogType.Browser); | ||
| var styleErrors = logs.Where(log => log.Message.Contains(cspErrorMessage)).ToList(); | ||
|
ilonatommy marked this conversation as resolved.
|
||
| Assert.Empty(styleErrors); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.