Skip to content

Commit 25b4cf8

Browse files
committed
Add OAuthProxyHttpMessageHandlerBuilder helper
Change CreateOAuthClient to use helper and add SSRF protections Wire through SSRF protection in ProxyHttpMessageHandlerBuilder Add SSRF protection and proxy support to JetStream
1 parent ffedc29 commit 25b4cf8

8 files changed

Lines changed: 143 additions & 141 deletions

File tree

CHANGELOG.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717
* Added override on `ToString()` on `AtProtoCredential` to return a redacted string in case of accidental logging.
1818
* Added default SSRF protections to `AtProtoAgent`, `AtProtoHttpClient` and `AtProtoJetStream` with [idunno.Security.Ssrf](https://github.com/blowdart/idunno.Security.Ssrf/blob/main/src/idunno.Security.Ssrf/).
1919
This can be disabled by passing your own `HttpClient`.
20-
* Note: If you set a proxyUri that uses https://localhost or http://localhost then the SSRF protections will be disabled
21-
for localhost access automatically.
2220
* Added `AllowLoopback` parameter to `BuildOAuth2LoginUri` to allow loopback addresses in discovered URIs for testing and development purposes. This is disabled by default.
2321

2422
### idunno.AtProto.Types

Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<PackageVersion Include="DnsClient" Version="1.8.0" />
1212
<PackageVersion Include="Duende.IdentityModel.OidcClient" Version="7.0.1" />
1313
<PackageVersion Include="Duende.IdentityModel.OidcClient.Extensions" Version="7.0.1" />
14-
<PackageVersion Include="idunno.Security.Ssrf" Version="2.1.0-prerelease.ga732cc9dbc" />
14+
<PackageVersion Include="idunno.Security.Ssrf" Version="3.0.0" />
1515
<PackageVersion Include="JunitXml.TestLogger" Version="7.1.0" />
1616
<PackageVersion Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="3.3.4" />
1717
<PackageVersion Include="Microsoft.CodeCoverage" Version="18.3.0" />

src/idunno.AtProto/Agent.cs

Lines changed: 32 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -65,66 +65,13 @@ protected Agent(HttpClientOptions? httpClientOptions, JsonOptions? jsonOptions,
6565
JsonOptions = jsonOptions;
6666
}
6767

68-
bool checkCrl;
69-
if (httpClientOptions is null)
70-
{
71-
checkCrl = true;
72-
}
73-
else
74-
{
75-
checkCrl = httpClientOptions.CheckCertificateRevocationList;
76-
}
77-
7868
_loggerFactory = loggerFactory;
79-
80-
IServiceCollection services = new ServiceCollection();
8169
_httpClientOptions = httpClientOptions;
8270

83-
bool allowLoopback = false;
84-
bool allowInsecureProtocols = false;
85-
86-
IWebProxy? proxy = null;
87-
SslClientAuthenticationOptions? sslOptions = null;
88-
89-
if (_httpClientOptions?.ProxyUri is not null)
90-
{
91-
proxy = new WebProxy(_httpClientOptions.ProxyUri);
92-
if (_httpClientOptions.ProxyUri.IsLoopback)
93-
{
94-
allowLoopback = true;
95-
}
96-
97-
if (_httpClientOptions.ProxyUri.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase))
98-
{
99-
allowInsecureProtocols = true;
100-
}
101-
}
102-
103-
if (!checkCrl)
104-
{
105-
sslOptions = new SslClientAuthenticationOptions
106-
{
107-
CertificateRevocationCheckMode = X509RevocationMode.NoCheck
108-
};
109-
}
110-
71+
IServiceCollection services = new ServiceCollection();
11172
services
11273
.AddHttpClient(HttpClientName, client => InternalConfigureHttpClient(client, _httpClientOptions?.HttpUserAgent, _httpClientOptions?.Timeout))
113-
.ConfigurePrimaryHttpMessageHandler(() => SsrfSocketsHttpHandlerFactory.Create(
114-
connectionStrategy: ConnectionStrategy.None,
115-
additionalUnsafeNetworks: null,
116-
additionalUnsafeIpAddresses: null,
117-
connectTimeout: _httpClientOptions?.Timeout,
118-
allowInsecureProtocols: allowInsecureProtocols,
119-
allowLoopback: allowLoopback,
120-
failMixedResults: true,
121-
allowAutoRedirect: false,
122-
automaticDecompression: DecompressionMethods.All,
123-
proxy: proxy,
124-
sslOptions: sslOptions,
125-
loggerFactory: loggerFactory
126-
));
127-
74+
.ConfigurePrimaryHttpMessageHandler(ProxyHttpMessageHandlerBuilder);
12875
_serviceProvider = services.BuildServiceProvider();
12976

13077
HttpClientFactory = _serviceProvider.GetService<IHttpClientFactory>()!;
@@ -211,30 +158,11 @@ protected HttpClient ConfigureHttpClient(HttpClient client)
211158
/// <summary>
212159
/// Creates a client handler to configure proxy setup with the initialization parameters specified when creating the agent.
213160
/// </summary>
214-
/// <returns>An <see cref="SocketsHttpHandler"/> configured to any proxy specified when the agent was created.</returns>
215-
protected SocketsHttpHandler BuildProxyHttpClientHandler()
161+
/// <returns>An <see cref="HttpMessageHandler"/> configured to any proxy specified when the agent was created.</returns>
162+
protected HttpMessageHandler ProxyHttpMessageHandlerBuilder()
216163
{
217-
IWebProxy? proxy = null;
218164
SslClientAuthenticationOptions? sslOptions = null;
219165

220-
bool allowLoopback = false;
221-
bool allowInsecureProtocols = false;
222-
223-
if (_httpClientOptions?.ProxyUri is not null)
224-
{
225-
proxy = new WebProxy(_httpClientOptions.ProxyUri);
226-
227-
if (_httpClientOptions.ProxyUri.IsLoopback)
228-
{
229-
allowLoopback = true;
230-
}
231-
232-
if (_httpClientOptions.ProxyUri.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase))
233-
{
234-
allowInsecureProtocols = true;
235-
}
236-
}
237-
238166
if (_httpClientOptions?.CheckCertificateRevocationList == false)
239167
{
240168
sslOptions = new SslClientAuthenticationOptions
@@ -243,19 +171,34 @@ protected SocketsHttpHandler BuildProxyHttpClientHandler()
243171
};
244172
}
245173

246-
return SsrfSocketsHttpHandlerFactory.Create(
247-
connectionStrategy: ConnectionStrategy.None,
248-
additionalUnsafeNetworks: null,
249-
additionalUnsafeIpAddresses: null,
250-
connectTimeout: _httpClientOptions?.Timeout,
251-
allowInsecureProtocols: allowInsecureProtocols,
252-
allowLoopback: allowLoopback,
253-
failMixedResults: true,
254-
allowAutoRedirect: false,
255-
automaticDecompression: DecompressionMethods.All,
256-
proxy: proxy,
257-
sslOptions: sslOptions,
258-
loggerFactory: _loggerFactory);
174+
SsrfOptions options = new()
175+
{
176+
ConnectionStrategy = ConnectionStrategy.None,
177+
AdditionalUnsafeNetworks = [],
178+
AdditionalUnsafeIpAddresses = [],
179+
ConnectTimeout = _httpClientOptions?.Timeout,
180+
AllowInsecureProtocols = false,
181+
AllowLoopback = false,
182+
FailMixedResults = true,
183+
AllowAutoRedirect = false,
184+
AutomaticDecompression = DecompressionMethods.All,
185+
SslOptions = sslOptions
186+
};
187+
188+
189+
if (_httpClientOptions?.ProxyUri is not null)
190+
{
191+
options.Proxy = new WebProxy(_httpClientOptions.ProxyUri);
192+
return new ProxiedSsrfDelegatingHandler(
193+
options: options,
194+
loggerFactory: _loggerFactory);
195+
}
196+
else
197+
{
198+
return SsrfSocketsHttpHandlerFactory.Create(
199+
options: options,
200+
loggerFactory: _loggerFactory);
201+
}
259202
}
260203

261204
private static void InternalConfigureHttpClient(HttpClient client, string? httpUserAgent = null, TimeSpan? timeout = null)

src/idunno.AtProto/AtProtoHttpClient.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ public class AtProtoHttpClient(string? serviceProxy = null, ILoggerFactory? logg
4343
failMixedResults: true,
4444
allowAutoRedirect: false,
4545
automaticDecompression: DecompressionMethods.All,
46-
proxy: null,
4746
sslOptions: null,
4847
loggerFactory: loggerFactory);
4948

src/idunno.AtProto/Authentication/AtProtoAgent.cs

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33

44
using System.Diagnostics.CodeAnalysis;
55
using System.Net;
6+
using System.Net.Security;
67
using System.Security.Cryptography;
8+
using System.Security.Cryptography.X509Certificates;
79
using System.Text;
810
using System.Text.Json;
911
using System.Timers;
@@ -15,6 +17,7 @@
1517

1618
using idunno.AtProto.Authentication;
1719
using idunno.AtProto.Events;
20+
using idunno.Security;
1821

1922
namespace idunno.AtProto;
2023

@@ -144,7 +147,7 @@ protected internal virtual void InternalOnCredentialsUpdatedCallBack(AtProtoCred
144147
/// <returns>The new <see cref="OAuthClient"/> instance.</returns>
145148
public OAuthClient CreateOAuthClient()
146149
{
147-
return new OAuthClient(ConfigureHttpClient, BuildProxyHttpClientHandler, LoggerFactory, Options?.OAuthOptions);
150+
return new OAuthClient(ConfigureHttpClient, OAuthProxyHttpMessageHandlerBuilder, LoggerFactory, Options?.OAuthOptions);
148151
}
149152

150153
/// <summary>
@@ -157,14 +160,67 @@ public OAuthClient CreateOAuthClient(OAuthLoginState state)
157160
{
158161
ArgumentNullException.ThrowIfNull(state);
159162

160-
var oAuthClient = new OAuthClient(ConfigureHttpClient, BuildProxyHttpClientHandler, LoggerFactory, Options?.OAuthOptions)
163+
var oAuthClient = new OAuthClient(ConfigureHttpClient, OAuthProxyHttpMessageHandlerBuilder, LoggerFactory, Options?.OAuthOptions)
161164
{
162165
State = state
163166
};
164167

165168
return oAuthClient;
166169
}
167170

171+
/// <summary>
172+
/// Builds a preconfigured <see cref="HttpMessageHandler"/> to use for making requests during the OAuth flow, with SSRF protections in place.
173+
/// If a proxy is configured in <see cref="Options"/>, the handler will be configured to route requests through the proxy while still applying SSRF protections
174+
/// to the ultimate endpoints being called.
175+
/// If the OAuth return URI configured in <see cref="Options"/> is using HTTP or is a loopback address, the handler will be configured to allow insecure protocols or
176+
/// loopback addresses respectively, but will still apply SSRF protections to all other endpoints.
177+
/// </summary>
178+
/// <returns>A preconfigured <see cref="HttpMessageHandler"/> instance.</returns>
179+
private HttpMessageHandler OAuthProxyHttpMessageHandlerBuilder()
180+
{
181+
SslClientAuthenticationOptions? sslOptions = null;
182+
183+
if (_httpClientOptions?.CheckCertificateRevocationList == false)
184+
{
185+
sslOptions = new SslClientAuthenticationOptions
186+
{
187+
CertificateRevocationCheckMode = X509RevocationMode.NoCheck
188+
};
189+
}
190+
191+
bool allowInsecureProtocols = Options?.OAuthOptions?.ReturnUri != null && Options.OAuthOptions.ReturnUri.Scheme == Uri.UriSchemeHttp;
192+
bool allowLoopback = Options?.OAuthOptions?.ReturnUri != null && Options.OAuthOptions.ReturnUri.IsLoopback;
193+
194+
SsrfOptions options = new()
195+
{
196+
ConnectionStrategy = ConnectionStrategy.None,
197+
AdditionalUnsafeNetworks = [],
198+
AdditionalUnsafeIpAddresses = [],
199+
ConnectTimeout = _httpClientOptions?.Timeout,
200+
AllowInsecureProtocols = allowInsecureProtocols,
201+
AllowLoopback = allowLoopback,
202+
FailMixedResults = true,
203+
AllowAutoRedirect = false,
204+
AutomaticDecompression = DecompressionMethods.All,
205+
SslOptions = sslOptions
206+
};
207+
208+
209+
if (_httpClientOptions?.ProxyUri is not null)
210+
{
211+
options.Proxy = new WebProxy(_httpClientOptions.ProxyUri);
212+
return new ProxiedSsrfDelegatingHandler(
213+
options: options,
214+
loggerFactory: LoggerFactory);
215+
}
216+
else
217+
{
218+
return SsrfSocketsHttpHandlerFactory.Create(
219+
options: options,
220+
loggerFactory: LoggerFactory);
221+
}
222+
}
223+
168224
/// <summary>
169225
/// Builds an OAuth authorization URI for starting the OAuth flow.
170226
/// </summary>

src/idunno.AtProto/Authentication/OAuthClient.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public class OAuthClient
3030
private OidcClient? _oidcClient;
3131

3232
private readonly Func<HttpClient, HttpClient> _clientConfigurationHandler = (httpClient) => { return httpClient; };
33-
private readonly Func<SocketsHttpHandler> _innerFactoryHandler = () => { throw new OAuthException("Handler factory not configured"); };
33+
private readonly Func<HttpMessageHandler> _innerFactoryHandler = () => { throw new OAuthException("Handler factory not configured"); };
3434
private readonly ILoggerFactory _loggerFactory;
3535
private readonly ILogger<OAuthClient> _logger;
3636

@@ -53,7 +53,7 @@ private OAuthClient(ILoggerFactory? loggerFactory = null, OAuthOptions? options
5353

5454
internal OAuthClient(
5555
Func<HttpClient, HttpClient> httpClientConfigurator,
56-
Func<SocketsHttpHandler> innerHandlerFactory,
56+
Func<HttpMessageHandler> innerHandlerFactory,
5757
ILoggerFactory? loggerFactory = null,
5858
OAuthOptions? options = null) : this(loggerFactory, options)
5959
{
@@ -329,7 +329,7 @@ public async Task<Uri> BuildOAuth2LoginUri(
329329

330330
AtProtoHttpResult<ServerDescription> serverDescriptionResult;
331331

332-
using (SocketsHttpHandler handler = _innerFactoryHandler())
332+
using (HttpMessageHandler handler = _innerFactoryHandler())
333333
using (var httpClient = new HttpClient(handler))
334334
{
335335
_clientConfigurationHandler(httpClient);
@@ -529,7 +529,7 @@ internal async Task<Uri> BuildOAuth2LogoutUri(
529529
}
530530

531531
AtProtoHttpResult<ServerDescription> serverDescriptionResult;
532-
using (SocketsHttpHandler handler = _innerFactoryHandler())
532+
using (HttpMessageHandler handler = _innerFactoryHandler())
533533
using (var httpClient = new HttpClient(handler))
534534
{
535535
_clientConfigurationHandler(httpClient);

0 commit comments

Comments
 (0)