Skip to content

Commit a5132da

Browse files
committed
Fix and secure WebView2 proxy authentication handling
Fix proxy authentication that was not working because the URI match was incorrectly comparing against the telemetry endpoint instead of the configured proxy. Extract the logic into a dedicated WebView2ProxyAuthHandler class that validates challenges originate from the trusted proxy before providing credentials.
1 parent 02152e9 commit a5132da

3 files changed

Lines changed: 114 additions & 18 deletions

File tree

src/GlobalUsings.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
global using System.Diagnostics.CodeAnalysis;
55
global using System.IO;
66
global using System.Linq;
7+
global using System.Net;
78
global using System.Net.Mime;
89
global using System.Text;
910
global using System.Text.Json;

src/Infrastructure/AppWindow.cs

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
using Sqlbi.Bravo.Infrastructure.Extensions;
1111
using Sqlbi.Bravo.Infrastructure.Helpers;
1212
using Sqlbi.Bravo.Infrastructure.Messages;
13-
using Sqlbi.Bravo.Infrastructure.Security;
1413
using Sqlbi.Bravo.Infrastructure.Services;
1514
using Sqlbi.Bravo.Infrastructure.Telemetry;
1615
using Sqlbi.Bravo.Infrastructure.Windows.Interop;
@@ -36,13 +35,15 @@ internal partial class AppWindow : Form
3635
private readonly AppInstance _instance;
3736
private readonly IServerAddressProvider _serverAddressProvider;
3837
private readonly IOptions<StartupSettings> _startupSettingsOptionsAccessor;
38+
private readonly WebView2ProxyAuthHandler _proxyAuthHandler;
3939
private readonly Color _startupThemeColor;
4040

4141
public AppWindow(IServiceProvider services, AppInstance instance)
4242
{
4343
_instance = instance;
4444
_serverAddressProvider = services.GetRequiredService<IServerAddressProvider>();
4545
_startupSettingsOptionsAccessor = services.GetRequiredService<IOptions<StartupSettings>>();
46+
_proxyAuthHandler = new WebView2ProxyAuthHandler(WebProxyWrapper.Current);
4647
_startupThemeColor = ThemeHelper.ShouldUseDarkMode(UserPreferences.Current.Theme) ? AppEnvironment.ThemeColorDark : AppEnvironment.ThemeColorLight;
4748

4849
UISynchronizationContext = new WindowsFormsSynchronizationContext();
@@ -201,23 +202,12 @@ private void OnWebViewPermissionRequested(object? sender, CoreWebView2Permission
201202

202203
private void OnWebViewBasicAuthenticationRequested(object? sender, CoreWebView2BasicAuthenticationRequestedEventArgs e)
203204
{
204-
var proxy = UserPreferences.Current.Proxy;
205-
if (proxy?.Type != ProxyType.None && proxy?.UseDefaultCredentials == false)
206-
{
207-
if (TelemetrySessionInfo.IsTelemetryUri(e.Uri))
208-
{
209-
if (CredentialManager.TryGetCredential(targetName: AppEnvironment.CredentialManagerProxyCredentialName, out var genericCredential))
210-
{
211-
var credential = genericCredential.ToNetworkCredential();
212-
if (credential is not null)
213-
{
214-
e.Response.UserName = credential.UserName;
215-
e.Response.Password = credential.Password;
216-
return;
217-
}
218-
}
219-
}
220-
}
205+
if (_proxyAuthHandler.TryHandle(e))
206+
return;
207+
208+
// Proxy authentication not handled (untrusted proxy or credentials unavailable).
209+
// Do not cancel the request — allow WebView's native auth dialog so the user can provide or deny credentials.
210+
e.Cancel = false;
221211

222212
//var deferral = e.GetDeferral();
223213

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
namespace Sqlbi.Bravo.Infrastructure.Services;
2+
3+
using Microsoft.Web.WebView2.Core;
4+
using Sqlbi.Bravo.Infrastructure.Configuration;
5+
using Sqlbi.Bravo.Infrastructure.Configuration.Settings;
6+
using Sqlbi.Bravo.Infrastructure.Security;
7+
using Sqlbi.Bravo.Infrastructure.Telemetry;
8+
9+
/// <summary>
10+
/// Handles WebView2 proxy authentication challenges (HTTP 407).
11+
///
12+
/// The WebView2-hosted UI communicates only with the local Kestrel server (localhost),
13+
/// which bypasses the proxy. The only external HTTP traffic originating from the WebView2
14+
/// is the Application Insights telemetry sent by the TypeScript UI layer directly to the
15+
/// ingestion endpoint over the internet.
16+
///
17+
/// Therefore, proxy authentication challenges in this context can only be triggered by
18+
/// telemetry requests going through the configured proxy.
19+
///
20+
/// Security: before providing credentials, we verify that the authentication challenge
21+
/// comes from the user's configured proxy — not from an arbitrary or rogue server. This
22+
/// prevents credential leakage to untrusted endpoints (e.g. a server returning a 401 to
23+
/// harvest proxy credentials).
24+
/// </summary>
25+
internal sealed class WebView2ProxyAuthHandler
26+
{
27+
private readonly IWebProxy _webProxy;
28+
29+
public WebView2ProxyAuthHandler(IWebProxy webProxy)
30+
{
31+
_webProxy = webProxy;
32+
}
33+
34+
/// <summary>
35+
/// Attempts to handle a proxy authentication challenge.
36+
/// Returns true if credentials were provided, false otherwise.
37+
/// </summary>
38+
public bool TryHandle(CoreWebView2BasicAuthenticationRequestedEventArgs e)
39+
{
40+
var proxy = UserPreferences.Current.Proxy;
41+
42+
// No proxy configured
43+
if (proxy is null)
44+
return false;
45+
46+
// Proxy explicitly disabled
47+
if (proxy.Type == ProxyType.None)
48+
return false;
49+
50+
// Proxy configured to use system credentials, we can't handle the challenge
51+
if (proxy.UseDefaultCredentials)
52+
return false;
53+
54+
// Verify the auth challenge comes from the configured proxy, not from an arbitrary server.
55+
if (!IsTrustedProxy(e.Uri))
56+
return false;
57+
58+
if (CredentialManager.TryGetCredential(targetName: AppEnvironment.CredentialManagerProxyCredentialName, out var genericCredential))
59+
{
60+
var credential = genericCredential.ToNetworkCredential();
61+
if (credential is not null)
62+
{
63+
e.Response.UserName = credential.UserName;
64+
e.Response.Password = credential.Password;
65+
return true;
66+
}
67+
}
68+
69+
return false;
70+
}
71+
72+
/// <summary>
73+
/// Verifies that the URI from the authentication challenge matches the proxy that
74+
/// the system would use for the telemetry ingestion endpoint.
75+
///
76+
/// This works for both Custom and System proxy types: <see cref="IWebProxy.GetProxy"/>
77+
/// resolves the correct proxy URI in both cases (including PAC/WPAD auto-discovery
78+
/// for system proxies).
79+
///
80+
/// We compare only scheme, host, and port — the path component is irrelevant for
81+
/// proxy identity verification.
82+
///
83+
/// Known limitation: the comparison is string-based and does not perform DNS resolution.
84+
/// If one URI uses a hostname (e.g. "proxy.contoso.com") and the other uses its IP address
85+
/// (e.g. "10.0.0.1"), the match will fail even though they refer to the same host.
86+
/// In practice this is unlikely because Chromium uses the same proxy URI that was passed
87+
/// via --proxy-server, and GetProxy() for Custom type returns the same user-configured address.
88+
/// For System proxies with PAC files the risk is slightly higher but remains an edge case.
89+
/// </summary>
90+
private bool IsTrustedProxy(string requestUri)
91+
{
92+
var expectedProxyUri = _webProxy.GetProxy(TelemetrySessionInfo.DefaultIngestionEndpoint);
93+
if (expectedProxyUri is null)
94+
return false;
95+
96+
var requestedUri = new Uri(requestUri);
97+
98+
return Uri.Compare(
99+
requestedUri,
100+
expectedProxyUri,
101+
UriComponents.Scheme | UriComponents.HostAndPort,
102+
UriFormat.Unescaped,
103+
StringComparison.OrdinalIgnoreCase) == 0;
104+
}
105+
}

0 commit comments

Comments
 (0)