Skip to content

Consolidate file path resolution for WebView controls#34620

Merged
mattleibow merged 5 commits intomainfrom
refactor/consolidate-file-loading
Apr 13, 2026
Merged

Consolidate file path resolution for WebView controls#34620
mattleibow merged 5 commits intomainfrom
refactor/consolidate-file-loading

Conversation

@mattleibow
Copy link
Copy Markdown
Member

Note

Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!

Description

Refactored scattered file-path resolution logic in HybridWebView and BlazorWebView into shared utilities (FileSystemUtils.Combine, WebUtils.ResolveRelativePath) to fix edge cases where Path.Combine drops the root directory when a relative path starts with a separator.

Changes

  • FileSystemUtils.shared.cs — Added IsValidRelativePath() and Combine() methods that validate relative paths and ensure the resolved path stays within the expected root directory
  • WebUtils.shared.cs — Added ResolveRelativePath() to resolve request URIs against an app origin into validated relative paths
  • HybridWebView handlers (iOS, Windows, Android) — Use the new shared utilities for file path resolution
  • BlazorWebView file providers (iOS, Android, Windows, Tizen) — Use FileSystemUtils.Combine() for path resolution
  • Email.windows.cs — Consolidated duplicate NormalizePath into FileSystemUtils.NormalizePath
  • Device tests — Added path resolution tests for both HybridWebView and BlazorWebView covering relative paths, rooted paths, dot-dot segments, and encoded separators

Copilot AI review requested due to automatic review settings March 24, 2026 17:03
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 24, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://github.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34620

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://github.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34620"

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors WebView-related URL/path resolution across HybridWebView and BlazorWebView to use shared helpers, aiming to prevent Path.Combine from dropping the intended root when given separator-prefixed “relative” paths, and to harden against path traversal.

Changes:

  • Added shared helpers for request-URI → relative-path resolution (WebUtils.ResolveRelativePath) and for validating/combining root + relative paths (FileSystemUtils.IsValidRelativePath/Combine).
  • Updated HybridWebView handlers (Android/iOS/Windows) and BlazorWebView asset file providers/managers (Android/iOS/Windows/Tizen) to use the shared helpers.
  • Added device tests and test assets covering relative paths, rooted paths, dot-dot segments, and encoded separators.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/Essentials/src/Types/Shared/WebUtils.shared.cs Adds ResolveRelativePath for validating app-origin-relative request paths.
src/Essentials/src/FileSystem/FileSystemUtils.shared.cs Adds IsValidRelativePath and Combine helper for safe root+relative path combination.
src/Essentials/src/Email/Email.windows.cs Switches attachment path normalization to FileSystemUtils.NormalizePath.
src/Core/src/Platform/Android/MauiHybridWebViewClient.cs Uses shared helpers for HybridWebView Android request path resolution + asset lookup.
src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.iOS.cs Uses shared helpers for iOS scheme-task request path resolution + asset lookup.
src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Windows.cs Uses shared helpers for Windows request path resolution + asset lookup.
src/Controls/tests/DeviceTests/Resources/Raw/HybridTestRoot/urlresolution.html Adds test page used to fetch URLs and report results to device tests.
src/Controls/tests/DeviceTests/Resources/Raw/HybridTestRoot/safe-file.txt Adds a known “safe” file for positive resolution assertions.
src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_ContentRootResolution.cs Adds HybridWebView device tests covering path resolution edge cases.
src/Controls/src/Core/Controls.Core.csproj Includes the shared FileSystemUtils.shared.cs source into Controls.Core for reuse.
src/BlazorWebView/tests/DeviceTests/Elements/BlazorWebViewTests.ContentRootResolution.cs Adds BlazorWebView device tests covering path resolution edge cases.
src/BlazorWebView/src/Maui/iOS/iOSMauiAssetFileProvider.cs Uses shared combine helper when serving bundled static assets (iOS).
src/BlazorWebView/src/Maui/Windows/WinUIWebViewManager.cs Uses shared ResolveRelativePath and combine helper for local folder serving (Windows).
src/BlazorWebView/src/Maui/Tizen/TizenMauiAssetFileProvider.cs Uses shared combine helper when serving packaged static assets (Tizen).
src/BlazorWebView/src/Maui/Android/AndroidMauiAssetFileProvider.cs Uses shared combine helper when serving APK assets (Android).

Refactored scattered file-path resolution logic into shared utilities
(FileSystemUtils.Combine, WebUtils.ResolveRelativePath) to fix edge
cases where Path.Combine drops the root directory when a relative path
starts with a separator. Added device tests for path resolution.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mattleibow mattleibow force-pushed the refactor/consolidate-file-loading branch from e19a666 to 2b1b2b8 Compare March 25, 2026 17:52
@github-actions
Copy link
Copy Markdown
Contributor

🧪 PR Test Evaluation

Overall Verdict: ⚠️ Tests need improvement

The device tests cover the critical path traversal attack vectors end-to-end, but the new security-critical utility methods (FileSystemUtils.IsValidRelativePath and FileSystemUtils.Combine) lack direct unit tests — a gap worth addressing for such foundational, cross-platform logic.

👍 / 👎 — Was this evaluation helpful? React to let us know!

📊 Expand Full Evaluation

PR Test Evaluation Report

PR: #34620 — Consolidate file path resolution for WebView controls
Test files evaluated: 2
Fix files: 10


Overall Verdict

⚠️ Tests need improvement

The device tests provide solid end-to-end coverage of the path traversal scenarios, but the core security utility methods introduced in FileSystemUtils are not directly unit-tested. Given their security-critical nature and the fact they are pure C# logic, they deserve targeted unit tests in isolation.


1. Fix Coverage — ✅

The tests exercise the actual URL-to-file resolution pipeline that was refactored, including the new FileSystemUtils.IsValidRelativePath and FileSystemUtils.Combine methods (via WebUtils.ResolveRelativePath and the platform handler chain). Both the BlazorWebView and HybridWebView paths are exercised. Test failures would surface if the fix were reverted.


2. Edge Cases & Gaps — ⚠️

Covered:

  • Rooted paths (//images/logo.png) — verified they don't resolve
  • Dot-dot traversal (../readme.txt, ../../data/config.txt, subfolder/../../readme.txt) — verified they don't resolve
  • URL-encoded separators (%2F%2F, %2e%2e) — verified they don't bypass resolution
  • Happy path — relative paths inside content root return content
  • Known file content check (safe-file.txt body is asserted in KnownFile_ReturnsExpectedContent)

Missing:

  • FileSystemUtils.Combine case-sensitivity on WindowsStartsWith uses StringComparison.Ordinal, which is case-sensitive. On Windows (case-insensitive FS), a path like C:\Root\subdir vs c:\root\subdir could produce a false rejection. No test exercises mixed-case root on Windows.
  • Single dot segment (./readme.txt) — currently passes IsValidRelativePath (no .. check) and should resolve; no test confirms this works correctly.
  • Empty string relative pathIsValidRelativePath returns true for empty string; Path.Combine(root, "") returns root, which passes the boundary check. This edge case has no test to confirm the behavior is intentional.
  • Tizen platformTizenMauiAssetFileProvider.cs was changed but there are no Tizen tests (acceptable given Tizen CI constraints, but worth noting).
  • Direct unit tests for FileSystemUtils.IsValidRelativePath and FileSystemUtils.Combine — these pure methods are testable without any platform context and would provide faster, more targeted coverage of the security validation logic.

3. Test Type Appropriateness — ⚠️

Current: Device Tests (xUnit via XHarness/Helix)
Recommendation: Add unit tests for utility methods alongside existing device tests

FileSystemUtils.IsValidRelativePath and FileSystemUtils.Combine are pure C# methods with no platform dependencies (no Android, iOS, or Windows APIs). They are ideal candidates for unit tests in src/Essentials/test/UnitTests/FileSystem_Tests.cs, which already exists and tests other FileSystemUtils helpers.

The device tests are appropriate and should be kept — they verify the full request interception stack. But unit tests would be faster to run in CI, easier to add edge-case coverage to, and provide clearer failure messages when the logic breaks.


4. Convention Compliance — ✅

No convention issues detected by the automated script.

  • HybridWebViewTests_ContentRootResolution.cs correctly uses [Category(TestCategory.HybridWebView)], inherits HybridWebViewTestsBase, and uses [Fact]/[Theory]/[InlineData] xUnit attributes.
  • BlazorWebViewTests.ContentRootResolution.cs is a partial class consistent with the existing BlazorWebViewTests pattern.
  • No Task.Delay/Thread.Sleep usage found.
  • No obsolete API usage.

5. Flakiness Risk — ⚠️ Medium

  • BlazorWebView assertions are lenient — negative-case assertions use Assert.True(result.status != 200 || IsSpaFallback(result)). This means if a rooted path accidentally returns a 200 with SPA fallback content, the test would pass even though a security regression occurred. The IsSpaFallback escape hatch reduces test fidelity.
  • HybridWebView assertions are tightAssert.NotEqual(200, result.status) is clear and will catch regressions. No flakiness concerns here.
  • No arbitrary delays or timing-dependent logic observed.

6. Duplicate Coverage — ✅ No duplicates

Existing tests (HybridWebViewFeatureTests.cs, Issue30846.cs) test general HybridWebView functionality. The new *_ContentRootResolution tests specifically target path traversal prevention, which was not previously covered.


7. Platform Scope — ⚠️

Platform Fix Files Test Coverage
Android AndroidMauiAssetFileProvider.cs, MauiHybridWebViewClient.cs ✅ Via device tests
iOS iOSMauiAssetFileProvider.cs, HybridWebViewHandler.iOS.cs ✅ Via device tests
Windows WinUIWebViewManager.cs, HybridWebViewHandler.Windows.cs, Email.windows.cs ✅ Via device tests
MacCatalyst Shared with iOS ✅ Via iOS device tests (.ios.cs compiles for both)
Tizen TizenMauiAssetFileProvider.cs ❌ No Tizen tests
Shared/Cross-platform FileSystemUtils.shared.cs, WebUtils.shared.cs, Email.windows.cs ⚠️ Only tested through platform device tests

The Essentials/src/Email/Email.windows.cs change also uses the new FileSystemUtils.Combine — this is only exercised if Windows email attachment tests exist elsewhere.


8. Assertion Quality — ⚠️

Test Assertion Quality
RelativePaths_ResolveToContent Assert.Equal(200, ...) + body length ✅ Specific
KnownFile_ReturnsExpectedContent Checks actual body content ✅ Excellent
RootedPath_DoesNotResolve (HybridWebView) Assert.NotEqual(200, ...) ✅ Clear
DotDotSegments_DoNotResolveAboveRoot (HybridWebView) Assert.NotEqual(200, ...) ✅ Clear
Blazor_RootedPath_DoesNotResolve `status != 200
Blazor_DotDotSegments_DoNotResolveAboveRoot `status != 200

9. Fix-Test Alignment — ✅

Fix files and test files align well: BlazorWebViewTests.ContentRootResolution.cs tests the BlazorWebView asset provider changes, and HybridWebViewTests_ContentRootResolution.cs tests the HybridWebView handler changes. Both exercise the shared FileSystemUtils and WebUtils logic through the request pipeline.


Recommendations

  1. Add unit tests for FileSystemUtils.IsValidRelativePath and FileSystemUtils.Combine in src/Essentials/test/UnitTests/FileSystem_Tests.cs. These methods are the security foundation of this PR — test them directly with edge cases like empty string, single dot ./, null, backslash-only paths, and null byte injection (\0).

  2. Tighten BlazorWebView negative assertions — replace Assert.True(result.status != 200 || IsSpaFallback(result)) with Assert.NotEqual(200, result.status) where possible, or add a comment explaining why SPA fallback behavior is intentionally acceptable for these paths. As written, the assertion could silently pass even if a traversal attack succeeds but returns HTML.

  3. Add a Windows case-sensitivity test — verify that FileSystemUtils.Combine behaves correctly when root and path have different casing on Windows (e.g., C:\Root\Content vs c:\root\content\file.txt), since StringComparison.Ordinal is case-sensitive and Windows paths are not.

Warning

⚠️ Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • dc.services.visualstudio.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "dc.services.visualstudio.com"

See Network Configuration for more information.

Note

🔒 Integrity filtering filtered 2 items

Integrity filtering activated and filtered the following items during workflow execution.
This happens when a tool call accesses a resource that does not meet the required integrity or secrecy level of the workflow.

🧪 Test evaluation by Evaluate PR Tests

- Fix IsValidRelativePath to check '..' per-segment instead of substring
- Fix Combine to preserve relative roots for Android/package asset paths
- Fix Combine to use case-insensitive comparison on Windows
- Remove unused using directive in WebUtils.shared.cs
- Rename test methods and comments for clarity
- Add FileSystemUtils and WebUtils unit tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

🧪 PR Test Evaluation

Overall Verdict: ✅ Tests are adequate

The PR adds strong, well-layered test coverage for the path resolution security fix — unit tests cover the logic exhaustively, and device tests validate the fix end-to-end inside real WebView instances. One minor assertion in WebUtils_Tests is slightly permissive, but the overall quality is high.

👍 / 👎 — Was this evaluation helpful? React to let us know!

📊 Expand Full Evaluation

PR Test Evaluation Report

PR: #34620 — Consolidate file path resolution for WebView controls
Test files evaluated: 4
Fix files: 10


Overall Verdict

Tests are adequate

The PR provides excellent layered coverage: pure unit tests for the new FileSystemUtils and WebUtils utility methods (22 + 7 test cases), plus device tests that confirm the security fix holds end-to-end inside HybridWebView and BlazorWebView.


1. Fix Coverage — ✅

The tests directly exercise the fixed code paths:

  • Unit tests target the new IsValidRelativePath and Combine helpers added to FileSystemUtils.shared.cs, and the new ResolveRelativePath in WebUtils.shared.cs — these are the exact functions the handler code now delegates to.
  • Device tests (HybridWebViewTests_ContentRootResolution, BlazorWebViewTests.ContentRootResolution) use real fetch() requests through the WebView to verify that previously-exploitable paths (e.g., //images/logo.png, ../readme.txt) now return non-200 responses.
  • The device tests include a KnownFile_ReturnsExpectedContent test that confirms legitimate requests still succeed after the fix, guarding against regressions.
  • The fix in Email.windows.cs (removing the local NormalizePath duplicate in favor of FileSystemUtils.NormalizePath) is a safe refactor; the behavior is identical and is covered transitively.

2. Edge Cases & Gaps — ✅

Covered:

  • Null and empty paths (IsValidRelativePath_NullOrEmpty_ReturnsTrue)
  • Valid relative paths with forward and backslash separators
  • Double-dot as a filename substring vs. as a path segment (foo..bar.js allowed; ../ rejected) — a subtle edge case that is explicitly tested
  • Path traversal with backslashes (..\\readme.txt)
  • Rooted paths with single/double/triple leading slashes
  • Windows drive letters (C:\\)
  • Network-style paths (///server/share)
  • Absolute vs. relative root directory behavior (important for Android asset paths)
  • Windows case-insensitive path comparison
  • URL-encoded separators (%2e%2e, %2F%2F)
  • Single-dot (./) segments

Potential minor gap:

  • WebUtils_Tests.ResolveRelativePath_DoubleSlash_MakeRelativeUri_ProducesRooted_ReturnsNull uses a conditional assertion: if (result is not null) { Assert.False(...) } — the test passes whether result is null or a valid path. While this is intentional to handle platform differences in Uri.MakeRelativeUri, it means the test cannot catch a regression where the method returns a rooted path silently.

3. Test Type Appropriateness — ✅

Layer Test Type Verdict
FileSystemUtils path logic Unit tests (xUnit) ✅ Optimal — pure logic, no platform dependency
WebUtils.ResolveRelativePath Unit tests (xUnit) ✅ Optimal — URI manipulation is cross-platform
HybridWebView handler integration Device tests ✅ Appropriate — needs actual WebView and file system
BlazorWebView integration Device tests ✅ Appropriate — needs actual WebView and Blazor runtime

No UI tests (Appium) were added, which is correct — the fix is in file-serving infrastructure, not visual UI.


4. Convention Compliance — ✅

Automated script found 0 convention issues.

  • Unit tests use [Fact] and [Theory] (xUnit) ✅
  • Device tests use [Fact] and [Theory] (xUnit) ✅
  • HybridWebViewTests_ContentRootResolution has [Category(TestCategory.HybridWebView)]
  • BlazorWebViewTests.ContentRootResolution is a partial class on BlazorWebViewTests, which carries [Category(TestCategory.BlazorWebView)] from the main class file ✅
  • #if WINDOWS [Collection(WebViewsCollection)] on the HybridWebView test class follows existing platform-specific collection conventions ✅

5. Flakiness Risk — ✅ Low

  • Unit tests are pure in-memory logic with zero I/O or timing dependencies.
  • Device tests use async/await with InvokeJavaScriptAsync (infrastructure-managed timeout) — no Task.Delay or Thread.Sleep.
  • No screenshot comparisons, no Appium interaction, no external URLs.
  • The BlazorWebView tests use IsSpaFallback() to handle legitimate SPA routing responses, avoiding false failures from the framework's own 200-with-host-page behavior.

6. Duplicate Coverage — ✅ No duplicates

The script identified HybridWebViewFeatureTests.cs and Issue30846.cs as existing HybridWebView tests. The new HybridWebViewTests_ContentRootResolution tests are specifically focused on path security (traversal, rooted paths, encoded segments) — a distinct scenario not covered by the existing feature/issue tests.


7. Platform Scope — ✅

The fix touches all four MAUI platforms (Android, iOS, MacCatalyst, Windows). Coverage:

Platform Fix Unit tests Device tests
Android MauiHybridWebViewClient.cs, AndroidMauiAssetFileProvider.cs ✅ (cross-platform) ✅ via device test suite
iOS / MacCatalyst HybridWebViewHandler.iOS.cs, iOSMauiAssetFileProvider.cs
Windows HybridWebViewHandler.Windows.cs, WinUIWebViewManager.cs
Tizen TizenMauiAssetFileProvider.cs ✅ (cross-platform) ⚠️ No Tizen device tests (expected — no Tizen CI)

The Tizen gap is acceptable; Tizen has no device test infrastructure in this repo.


8. Assertion Quality — ✅

Most assertions are well-targeted:

  • Assert.Equal(200, result.status) and Assert.NotEqual(200, result.status) — appropriate for HTTP response validation
  • Assert.Contains("content directory", result.bodyPreview) — confirms the correct file content is served, not just any 200
  • Assert.Null(result) for traversal attempts — direct
  • Assert.False(Path.IsPathRooted(result)) for relative root preservation — specific

Minor concern: WebUtils_Tests.ResolveRelativePath_DoubleSlash_... wraps its assertion in if (result is not null), meaning a null return silently passes. The comment explains this is intentional platform variance, but a future refactor could accidentally return a rooted value and this test wouldn't catch it. Consider splitting into explicit null-or-valid assertions.

The Blazor device test assertions use result.status != 200 || IsSpaFallback(result) — this is a necessary accommodation for SPA routing (not a quality issue) and is well-documented with the helper method.


9. Fix-Test Alignment — ✅

  • The unit tests target FileSystemUtils.IsValidRelativePath and FileSystemUtils.Combine — exactly the two new methods introduced by the fix.
  • The device tests confirm that the end-to-end path in each handler (iOS, Android, Windows) correctly rejects traversal and rooted paths.
  • The urlresolution.html test page uses fetch() to exercise the real request-handling pipeline in each platform's WebView, not a mocked path.
  • Email.windows.cs change is a pure refactor (uses FileSystemUtils.NormalizePath instead of a local duplicate) and doesn't need dedicated tests.

Recommendations

  1. Consider tightening the ResolveRelativePath_DoubleSlash test — instead of if (result is not null) { Assert.False(...) }, use Assert.True(result is null || !Path.IsPathRooted(result), "...") to make the conditional explicit and failure messages clearer.

  2. No additional tests needed — the unit + device coverage is comprehensive and appropriately layered. The PR is ready from a test quality standpoint.

Warning

⚠️ Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • dc.services.visualstudio.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "dc.services.visualstudio.com"

See Network Configuration for more information.

Note

🔒 Integrity filtering filtered 1 item

Integrity filtering activated and filtered the following item during workflow execution.
This happens when a tool call accesses a resource that does not meet the required integrity or secrecy level of the workflow.

🧪 Test evaluation by Evaluate PR Tests

…ompat

- Revert Tizen file provider changes (Tizen is no longer supported)
- Replace OperatingSystem.IsWindows() with OrdinalIgnoreCase comparison
  to fix CS0117 on netstandard2.0 builds
- Add null-forgiving operators for netstandard2.0 nullable analysis

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

🧪 PR Test Evaluation

Overall Verdict: ✅ Tests are adequate

The PR includes 22 unit tests and 14 device tests with comprehensive coverage of the core fix (path traversal prevention), edge cases, and cross-platform integration. Minor improvements noted below.

👍 / 👎 — Was this evaluation helpful? React to let us know!

📊 Expand Full Evaluation

PR Test Evaluation Report

PR: #34620 — Consolidate file path resolution for WebView controls
Test files evaluated: 4 (2 unit, 2 device)
Fix files: 9


Overall Verdict

Tests are adequate

The PR has comprehensive unit tests for the new FileSystemUtils and WebUtils utilities, plus device tests that exercise the full path through HybridWebView and BlazorWebView handlers. Coverage of edge cases is strong, with only minor gaps.


1. Fix Coverage — ✅

The tests directly exercise the two new shared utilities that form the heart of the fix:

  • FileSystemUtils_Tests tests IsValidRelativePath and Combine — the exact methods that replace scattered Path.Combine calls in the handlers
  • WebUtils_Tests tests ResolveRelativePath — which uses IsValidRelativePath to guard URL-to-path resolution
  • HybridWebViewTests_ContentRootResolution and BlazorWebViewTests.ContentRootResolution make real HTTP fetch requests through the handlers, verifying that rooted and traversal paths are blocked end-to-end

The tests would fail if the fix were reverted.


2. Edge Cases & Gaps — ✅

Covered:

  • Null / empty relative paths (returns true — valid as "no path")
  • Valid relative paths: simple, subdirectory, single-dot
  • .. as a path segment vs. .. inside a filename (foo..bar.js allowed, ../ blocked)
  • Rooted paths: Unix-style /file.txt, double-slash //path, triple-slash ///path
  • Windows drive letters (C:\, D:/) — with OS guard
  • Backslash dot-dot traversal (..\\)
  • Network-style paths (///127.0.0.1/share/)
  • Relative root directories (Android/asset-relative paths like wwwroot)
  • Slash normalization for current platform
  • Case-insensitive comparison on Windows
  • Encoded separators (%2F%2F, %2e%2e) in device tests
  • Different-origin rejection in ResolveRelativePath

Missing (minor):

  • WebUtils_Tests has no test for null appOriginUri or null requestUri (currently would NullReferenceException — if defensive behavior is expected, a test would document it)
  • IsValidRelativePath_RootedPath_ReturnsFalse does not test \file.txt (backslash-rooted, Windows-only) — Path.IsPathRooted returns true on Windows for this, but is not tested alongside drive-letter paths
  • Device tests for BlazorWebView negative cases (Blazor_DotDotSegments_DoNotResolveAboveRoot, Blazor_RootedPath_DoesNotResolve) accept SPA fallback responses as passing, which slightly reduces confidence that the path was actually blocked vs. the Blazor router interceded

None of these are blocking.


3. Test Type Appropriateness — ✅

Current: Unit Tests + Device Tests
Recommendation: Same — appropriate mix

The new utilities (FileSystemUtils, WebUtils) are pure logic with no platform dependencies, so unit tests are exactly right. The handler path resolution involves native file serving (iOS URL protocols, Android WebViewClient, Windows WebView2), making device tests the correct choice to verify end-to-end behavior. No UI tests are needed.


4. Convention Compliance — ✅

  • Unit tests use [Fact] / [Theory] with [InlineData] — correct xUnit conventions
  • Device tests (HybridWebViewTests_ContentRootResolution) have [Category(TestCategory.HybridWebView)] and inherit from HybridWebViewTestsBase — correct
  • BlazorWebViewTests.ContentRootResolution is a partial class extending the existing BlazorWebViewTests — correct pattern
  • #nullable enable on both unit test files
  • Script reported 0 convention issues

5. Flakiness Risk — ✅ Low

  • Unit tests: zero flakiness risk — pure .NET logic, no I/O, no async
  • Device tests: moderate inherent risk (JavaScript fetch over loopback), but uses the existing RunTest / ExecuteAsyncScriptAndWaitForResult infrastructure that handles timeouts and retries
  • No Task.Delay or Thread.Sleep in test code
  • One mild concern: IsSpaFallback string matching in BlazorWebView tests is fragile if host page content changes (e.g., blazor.webview.js renamed), but this is a well-established pattern in the codebase

6. Duplicate Coverage — ✅ No duplicates

Existing HybridWebViewFeatureTests and Issue30846 test general HybridWebView functionality, not path traversal. The new tests are the first to specifically cover Path.Combine boundary violation and URL traversal scenarios.


7. Platform Scope — ✅

Fix touches Windows, Android, iOS, and MacCatalyst handlers. Unit tests run on all platforms (no platform-specific code). Device tests cover:

  • HybridWebViewTests_ContentRootResolution: inherits HybridWebViewTestsBase, tagged [Category(TestCategory.HybridWebView)] → runs on Android, iOS, MacCatalyst, Windows
  • BlazorWebViewTests.ContentRootResolution: runs on Android, iOS, Windows

MacCatalyst is covered by the iOS device tests (same iOSMauiAssetFileProvider code path). The Email.windows.cs consolidation is covered implicitly as it re-uses FileSystemUtils.NormalizePath (tested in unit tests).


8. Assertion Quality — ✅

  • Assert.Equal(200, result.status) / Assert.NotEqual(200, ...) — specific status code checks
  • Assert.Contains("content directory", result.bodyPreview, ...) in KnownFile_ReturnsExpectedContent — verifies actual file content was served, not just a 200
  • Assert.Null / Assert.NotNull — correct and expected for nullable return values
  • Assert.StartsWith(root, result, ...) and Assert.EndsWith("file.txt", ...) — verifies path is within root
  • Minor concern: BlazorWebView negative-case assertions accept IsSpaFallback(result) as valid, meaning a 200 response with Blazor router content passes the test. This is pragmatic given SPA routing, but means "path was blocked" vs. "path was intercepted by Blazor router" is not distinguished. Documented behavior, low risk.

9. Fix-Test Alignment — ✅

Fix Component Test Coverage
FileSystemUtils.IsValidRelativePath FileSystemUtils_Tests — 22 cases
FileSystemUtils.Combine FileSystemUtils_Tests — extensive coverage
WebUtils.ResolveRelativePath WebUtils_Tests — valid, invalid, encoded paths
HybridWebViewHandler (iOS, Windows, Android) HybridWebViewTests_ContentRootResolution — end-to-end fetch tests
BlazorWebView file providers (iOS, Android, Windows) BlazorWebViewTests.ContentRootResolution — end-to-end fetch tests
Email.windows.cs NormalizePath consolidation Covered by NormalizePath_ReplacesForwardAndBackSlashes unit test

All fix components have corresponding tests.


Recommendations

  1. (Optional) Add a test for null URI parameters in WebUtils_Tests to document the expected behavior (throw vs. return null) and prevent unexpected behavior from callers
  2. (Optional) Add \file.txt to the IsValidRelativePath_RootedPath_ReturnsFalse theory data (Windows-only, alongside the existing drive-letter test)
  3. (Optional, low priority) Consider whether the BlazorWebView negative-case assertions could use a stricter check (e.g., Assert.NotEqual(200, ...) without the IsSpaFallback allowance) for the path traversal cases — the SPA fallback accepting those paths may still deserve a comment clarifying the behavior is intentional

Warning

⚠️ Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • dc.services.visualstudio.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "dc.services.visualstudio.com"

See Network Configuration for more information.

Note

🔒 Integrity filtering filtered 2 items

Integrity filtering activated and filtered the following items during workflow execution.
This happens when a tool call accesses a resource that does not meet the required integrity or secrecy level of the workflow.

🧪 Test evaluation by Evaluate PR Tests

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

@mattleibow
Copy link
Copy Markdown
Member Author

mattleibow commented Mar 27, 2026

/azp run maui-pr-devicetests

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 3 pipeline(s).

The Blazor host page is only served for navigation requests (ResourceContext.Document),
not for JavaScript fetch() requests. Calling fetch('') from within a BlazorWebView either
throws (unpackaged Windows) or hangs indefinitely (packaged Windows), causing CI failures.

The host page loading is already verified by the RunTest infrastructure which waits for
the Blazor component to render before running any test code.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

🧪 PR Test Evaluation

Overall Verdict: ⚠️ Tests need improvement

The unit tests are thorough and well-structured. The main gap is that BlazorWebView device tests lack a positive (happy-path) test confirming valid files still resolve correctly after the fix — without it, a regression that accidentally blocks legitimate requests would go undetected.

👍 / 👎 — Was this evaluation helpful? React to let us know!

📊 Expand Full Evaluation

PR Test Evaluation Report

PR: #34620 — Consolidate file path resolution for WebView controls
Test files evaluated: 4
Fix files: 9


Overall Verdict

⚠️ Tests need improvement

The unit tests for FileSystemUtils and WebUtils are comprehensive and well-written. HybridWebView device tests cover both positive and negative cases. However, the BlazorWebView device tests only cover negative cases (blocked paths), leaving a gap where regressions in normal file resolution would go undetected.


1. Fix Coverage — ✅

The fix replaces unsafe Path.Combine + Uri.MakeRelativeUri calls with FileSystemUtils.Combine and WebUtils.ResolveRelativePath across all platforms. The tests directly exercise these code paths:

  • Unit tests: FileSystemUtils_Tests and WebUtils_Tests directly test the new utility methods that underpin all platform changes
  • HybridWebView device tests: Exercise the actual HTTP interception path on all platforms (iOS, Android, Windows) using real WebView fetch calls
  • BlazorWebView device tests: Exercise the IFileProvider-based file resolution and Windows manager path logic

The Email.windows.cs change (replacing a local NormalizePath with FileSystemUtils.NormalizePath) is covered transitively by the NormalizePath_ReplacesForwardAndBackSlashes unit test. The behavior difference is minor: the old code only replaced /\, the new code also normalizes \\ (no-op on Windows), so no regression risk.


2. Edge Cases & Gaps — ⚠️

Covered:

  • Null/empty relative paths (IsValidRelativePath returns true for these)
  • Valid relative paths with forward slashes, subpaths, single dots
  • Filenames containing .. that are NOT path traversal segments (e.g., foo..bar.js)
  • Dot-dot segment path traversal (../, ../../, sub/../..)
  • Backslash-style dot-dot traversal (..\\)
  • Rooted paths (/file.txt, //images/logo.png)
  • Windows drive letters (C:\, D:/)
  • Network UNC-style paths (///127.0.0.1/share)
  • URL-encoded separators (%2e%2e, %2F%2F)
  • Case-insensitive comparison on Windows (root boundary check)
  • Relative root vs. absolute root distinction (Android package paths vs. absolute filesystem)

Missing:

  • BlazorWebView happy path: The BlazorWebView device tests only check that bad paths are blocked. There is no test asserting that a valid file (e.g., the Blazor app's own assets) still loads correctly after the fix. If FileSystemUtils.Combine were to return null for a valid path due to a bug, no BlazorWebView test would catch it.
  • MacCatalyst BlazorWebView explicit coverage: iOSMauiAssetFileProvider.cs compiles for both iOS and MacCatalyst, but the device test file doesn't explicitly mark MacCatalyst scenarios; this relies on CI infrastructure routing.
  • Encoded dot-dot via BlazorWebView: The unit test for %2e%2e uses a comment "Either null or a valid non-rooted path" — the assertion is permissive. If URI normalization were to decode %2e%2e into .. before IsValidRelativePath sees it, the test would still pass either way.

3. Test Type Appropriateness — ✅

Current: Unit Tests + Device Tests
Recommendation: Appropriate — no lighter type could cover this

  • FileSystemUtils and WebUtils logic is pure computation → unit tests ⭐ are exactly right
  • WebView path interception requires a running native WebView → device tests ⭐⭐ are the minimum viable type; UI tests would add unnecessary overhead

4. Convention Compliance — ✅

Automated script reported 0 convention issues. Manual check confirms:

  • Unit tests use [Fact] / [Theory] (xUnit) ✅
  • Device tests use [Category(TestCategory.HybridWebView)]
  • No Task.Delay / Thread.Sleep detected ✅
  • No obsolete API usage ✅

5. Flakiness Risk — ✅ Low

  • Unit tests: deterministic, no timing concerns
  • Device tests: use async/await properly with JS invocation; no arbitrary delays; the IsSpaFallback helper on BlazorWebView tests correctly handles Blazor's SPA routing returning the host page as a 200 response

6. Duplicate Coverage — ✅ No problematic overlap

Existing tests:

  • Issue30846.cs — an existing HybridWebView issue-specific UI test (different scenario)
  • HybridWebViewFeatureTests.cs — general feature matrix tests

The new tests add targeted path-resolution security coverage not present in existing tests.


7. Platform Scope — ⚠️

Fix files span all platforms:

Platform Fix files Device test coverage
Android AndroidMauiAssetFileProvider.cs, MauiHybridWebViewClient.cs ✅ HybridWebView device tests; BlazorWebView device tests
iOS iOSMauiAssetFileProvider.cs, HybridWebViewHandler.iOS.cs ✅ HybridWebView device tests; BlazorWebView device tests
MacCatalyst iOSMauiAssetFileProvider.cs (.ios.cs compiles for both) ⚠️ Implicit via .ios.cs; no explicit MacCatalyst test case
Windows WinUIWebViewManager.cs, HybridWebViewHandler.Windows.cs, Email.windows.cs ✅ HybridWebView device tests; BlazorWebView device tests

MacCatalyst is covered transitively via the .ios.cs file extension (which compiles for both iOS and MacCatalyst) and the Helix CI infra, but there are no explicit MacCatalyst test assertions.


8. Assertion Quality — ⚠️

  • Unit tests: ✅ Specific — Assert.True/False, Assert.Null/NotNull, Assert.Equal with concrete expected values
  • HybridWebView device tests: ✅ Specific — Assert.Equal(200, result.status), Assert.NotEqual(200, result.status), Assert.Contains("content directory", ...) directly tests the known file content
  • BlazorWebView device tests: ⚠️ Slightly permissive — assertions use result.status != 200 || IsSpaFallback(result). This is intentional (SPA fallback behavior) but means a traversal attack that returns the host page would pass the test. Consider adding an assertion on result.bodyPreview that it does NOT contain the actual traversed file's content, to harden the negative cases.

9. Fix-Test Alignment — ✅

All changed files map correctly to the tests:

  • FileSystemUtils.shared.csFileSystemUtils_Tests.cs (complete coverage)
  • WebUtils.shared.csWebUtils_Tests.cs (good coverage)
  • HybridWebView handlers (iOS, Android, Windows) → HybridWebViewTests_ContentRootResolution.cs
  • BlazorWebView file providers (iOS, Android) and Windows manager → BlazorWebViewTests.ContentRootResolution.cs
  • Email.windows.cs → covered by NormalizePath unit test (transitively)

Recommendations

  1. Add a positive test for BlazorWebView — add a test case in BlazorWebViewTests.ContentRootResolution.cs that fetches a known-good static asset (e.g., the Blazor wwwroot index or a test .txt file) and asserts it returns 200 with expected content. This ensures the fix doesn't accidentally block legitimate requests.

  2. Harden BlazorWebView negative assertions — in Blazor_DotDotSegments_DoNotResolveAboveRoot etc., consider additionally asserting that result.bodyPreview does not contain sensitive content (even if IsSpaFallback is true), to guard against a case where traversal returns the host page content instead of the intended blocked response.

  3. Consider an explicit Email.windows.cs integration test (low priority) — the change is a minor refactor, but a quick unit test asserting FileSystemUtils.NormalizePath behaves identically to the old inline method on the specific patterns used by Email attachments would make the refactor provably safe.

Warning

⚠️ Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • dc.services.visualstudio.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "dc.services.visualstudio.com"

See Network Configuration for more information.

Note

🔒 Integrity filtering filtered 2 items

Integrity filtering activated and filtered the following items during workflow execution.
This happens when a tool call accesses a resource that does not meet the required integrity or secrecy level of the workflow.

🧪 Test evaluation by Evaluate PR Tests

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 1 pipeline(s).

1 similar comment
@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 1 pipeline(s).

Fetches _framework/blazor.webview.js (always available in a BlazorWebView) and
asserts status 200 with content. This exercises ResolveRelativePath with a valid
multi-segment relative path and ensures the path hardening doesn't accidentally
block legitimate requests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

🧪 PR Test Evaluation

Overall Verdict: ✅ Tests are adequate

This PR adds a well-layered test suite (unit tests for the shared utilities + device tests for real WebView end-to-end behavior) that directly exercises the path traversal and rooted-path security fixes.

👍 / 👎 — Was this evaluation helpful? React to let us know!

📊 Expand Full Evaluation

PR Test Evaluation Report

PR: #34620 — Consolidate file path resolution for WebView controls
Test files evaluated: 4
Fix files: 9 (cross-platform: Android, iOS, Windows, MacCatalyst, shared utilities)


Overall Verdict

Tests are adequate

The PR takes a strong, layered approach: lightweight xUnit unit tests for the two new shared utilities (FileSystemUtils, WebUtils) and device tests for HybridWebView and BlazorWebView that verify the full end-to-end HTTP response behavior on real WebView rendering engines.


1. Fix Coverage — ✅

The unit tests in FileSystemUtils_Tests directly test IsValidRelativePath and Combine with the exact inputs that triggered the bug (rooted paths via //, path traversal via ../). WebUtils_Tests tests ResolveRelativePath, which is the call site used by the iOS/MacCatalyst handlers.

The device tests in HybridWebViewTests_ContentRootResolution and BlazorWebViewTests.ContentRootResolution confirm that the platform-level file providers (iOSMauiAssetFileProvider, AndroidMauiAssetFileProvider, WinUIWebViewManager, etc.) actually return non-200 responses for malicious paths. These tests would fail if the fix were reverted: Path.Combine(root, "//images/logo.png") returns //images/logo.png (drops the root), so the file would be served from the wrong location.


2. Edge Cases & Gaps — ✅

Covered:

  • ../ traversal (single, double, nested: sub/../../)
  • Rooted paths: /file.txt, //images/logo.png, ///data/
  • Windows UNC-style network paths: ///127.0.0.1/share/, ///localhost/C$/
  • Windows drive letters: C:\, D:/ (guarded with OperatingSystem.IsWindows())
  • Encoded traversal: %2e%2e/readme.txt, %2F%2Fimages/logo.png
  • Backslash variants: ..\readme.txt, subfolder\..\..
  • Filenames that legitimately contain .. but are NOT traversal segments: foo..bar.js, image..png
  • Relative roots (Android package/asset paths like wwwroot, HybridTestRoot) — ensures relative semantics are preserved
  • Single-dot . paths, empty relative paths
  • Slash normalization for relative roots
  • Positive "known-good file still loads" test for BlazorWebView (_framework/blazor.webview.js) and HybridWebView (safe-file.txt) — essential for preventing regressions

Minor gaps (non-blocking):

  • Email.windows.cs removes a local NormalizePath duplicate in favor of FileSystemUtils.NormalizePath. This change isn't explicitly tested, but the NormalizePath_ReplacesForwardAndBackSlashes unit test covers the shared method's behavior. The refactor is purely mechanical.
  • Tizen (TizenMauiAssetFileProvider.cs) was changed then reverted in the same PR (commit 98cbf91c), so no Tizen device tests are needed.
  • The DoubleSlash_MakeRelativeUri_ProducesRooted_ReturnsNull test in WebUtils_Tests has an "either null or non-rooted" soft assertion. For a security-relevant path, a stricter Assert.Null would be more appropriate if Uri normalizes the double-slash on all runtimes.

3. Test Type Appropriateness — ✅

Test Current Type Appropriate?
FileSystemUtils_Tests Unit (xUnit) ✅ Pure logic, no platform context needed
WebUtils_Tests Unit (xUnit) ✅ Pure logic, no platform context needed
HybridWebViewTests_ContentRootResolution Device Test ✅ Needs real WKWebView/WebView2/Chromium to verify HTTP status codes
BlazorWebViewTests.ContentRootResolution Device Test ✅ Needs real Blazor hosting + fetch() to verify response behavior

No UI tests were added (correct — no Appium-level interaction changed). The device tests are the lightest type that can verify actual HTTP response codes from the WebView interceptors.


4. Convention Compliance — ✅

No convention issues found by automated script.

  • Unit tests use [Fact] and [Theory] / [InlineData] correctly (xUnit)
  • Device tests use [Fact], [Theory], [Category(TestCategory.HybridWebView)] correctly
  • Windows-specific collection [Collection(WebViewsCollection)] applied correctly on HybridWebViewTests_ContentRootResolution
  • No Task.Delay/Thread.Sleep anti-patterns

5. Flakiness Risk — ✅ Low

  • Unit tests are fully deterministic
  • Device tests assert on HTTP status codes (200 vs not-200) returned by the WebView interceptor — deterministic given a fixed file system
  • Blazor_RootedPath_DoesNotResolve and Blazor_DotDotSegments_DoNotResolveAboveRoot correctly account for BlazorWebView's SPA fallback behavior using IsSpaFallback() — thoughtful design that avoids flakiness from SPA routing returning 200 for the host page

6. Duplicate Coverage — ✅ No duplicates

The existing HybridWebViewFeatureTests.cs and Issue30846.cs test general HybridWebView functionality. The new *ContentRootResolution tests are focused specifically on path security edge cases not covered by existing tests.


7. Platform Scope — ✅

The fix touches all 4 platforms (Android, iOS, Windows, MacCatalyst) plus shared utilities.

  • Unit tests compile and run cross-platform
  • HybridWebViewTests_ContentRootResolution: runs on Android, iOS, Windows, MacCatalyst via [Category(TestCategory.HybridWebView)]
  • BlazorWebViewTests.ContentRootResolution: runs on BlazorWebView's supported platforms

8. Assertion Quality — ✅

  • Unit tests assert exact boolean returns (Assert.True, Assert.False), exact null/non-null, exact string values — specific and meaningful
  • Combine_RelativeRoot_ValidRelative_ReturnsRelativePath asserts the result is NOT rooted — appropriate for the Android package-path use case
  • Device tests assert status == 200 for positive cases with bodyLength > 0, and status != 200 for negative cases
  • KnownFile_ReturnsExpectedContent asserts on actual file content ("content directory") — strongest possible assertion for a positive test

Minor concern: Combine_AbsoluteRoot_EmptyRelative_ReturnsRoot only asserts non-null without checking the actual return value. A stronger assertion would be Assert.Equal(Path.GetFullPath(root), result).


9. Fix-Test Alignment — ✅

Fix Component Tested By
FileSystemUtils.IsValidRelativePath FileSystemUtils_Tests (direct unit test)
FileSystemUtils.Combine FileSystemUtils_Tests (direct unit test)
WebUtils.ResolveRelativePath WebUtils_Tests (direct unit test)
iOSMauiAssetFileProvider BlazorWebViewTests.ContentRootResolution (device)
AndroidMauiAssetFileProvider BlazorWebViewTests.ContentRootResolution (device)
HybridWebViewHandler.iOS.cs HybridWebViewTests_ContentRootResolution (device)
MauiHybridWebViewClient.cs (Android) HybridWebViewTests_ContentRootResolution (device)
HybridWebViewHandler.Windows.cs HybridWebViewTests_ContentRootResolution (device)
WinUIWebViewManager.cs BlazorWebViewTests.ContentRootResolution (device)
Email.windows.cs (refactor) Covered by NormalizePath_ReplacesForwardAndBackSlashes unit test

Recommendations

  1. Consider tightening DoubleSlash_MakeRelativeUri_ProducesRooted_ReturnsNull — the current assertion (if (result is not null) Assert.False(IsPathRooted(result))) is soft. If Uri consistently decodes //images/logo.png to the same value across runtimes, Assert.Null(result) would be a stronger security assertion.

  2. Consider adding Assert.Equal(...) in Combine_AbsoluteRoot_EmptyRelative_ReturnsRoot — currently only asserts non-null; a stronger assertion on the actual returned path would prevent regressions.

  3. Low priority: For even more confidence on the Android Path.Combine bypass specifically, a unit test using a simulated absolute Android root path (/data/user/0/com.example/files/wwwroot) would pin down the cross-platform behavior of the boundary check.

Warning

⚠️ Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • dc.services.visualstudio.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "dc.services.visualstudio.com"

See Network Configuration for more information.

Note

🔒 Integrity filtering filtered 1 item

Integrity filtering activated and filtered the following item during workflow execution.
This happens when a tool call accesses a resource that does not meet the required integrity or secrecy level of the workflow.

🧪 Test evaluation by Evaluate PR Tests

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 1 pipeline(s).

@mattleibow mattleibow added this to the .NET 10 SR6 milestone Apr 9, 2026
@mattleibow mattleibow added the p/0 Current heighest priority issues that we are targeting for a release. label Apr 9, 2026
@mattleibow mattleibow merged commit f250a76 into main Apr 13, 2026
45 checks passed
@mattleibow mattleibow deleted the refactor/consolidate-file-loading branch April 13, 2026 22:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

p/0 Current heighest priority issues that we are targeting for a release.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants