Skip to content

Commit 75e436e

Browse files
Merge pull request from GHSA-67m4-qxp3-j6hh
fix: validated that IDs in input are not valid URIs
2 parents 9bf4120 + 7e0887f commit 75e436e

File tree

16 files changed

+343
-75
lines changed

16 files changed

+343
-75
lines changed

src/TrueLayer/ApiClient.cs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
using System.Net.Mime;
1010
using TrueLayer.Serialization;
1111
using System.Text.Json;
12+
using Microsoft.Extensions.Options;
13+
using TrueLayer.Common;
1214
using TrueLayer.Signing;
1315
#if NET6_0 || NET6_0_OR_GREATER
1416
using System.Net.Http.Json;
@@ -25,20 +27,23 @@ private static readonly String TlAgentHeader
2527
= $"truelayer-dotnet/{ReflectionUtils.GetAssemblyVersion<ITrueLayerClient>()}";
2628

2729
private readonly HttpClient _httpClient;
30+
private readonly TrueLayerOptions _options;
2831

2932
/// <summary>
3033
/// Creates a new <see cref="ApiClient"/> instance with the provided configuration, HTTP client factory and serializer.
3134
/// </summary>
3235
/// <param name="httpClient">The client used to make HTTP requests.</param>
33-
public ApiClient(HttpClient httpClient)
36+
/// <param name="options"></param>
37+
public ApiClient(HttpClient httpClient, IOptions<TrueLayerOptions> options)
3438
{
3539
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
40+
_options = options.Value ?? throw new ArgumentNullException(nameof(options));
3641
}
3742

3843
/// <inheritdoc />
3944
public async Task<ApiResponse<TData>> GetAsync<TData>(Uri uri, string? accessToken = null, CancellationToken cancellationToken = default)
4045
{
41-
if (uri is null) throw new ArgumentNullException(nameof(uri));
46+
uri.HasValidBaseUri(nameof(uri), _options);
4247

4348
using var httpResponse = await SendRequestAsync(
4449
httpMethod: HttpMethod.Get,
@@ -56,7 +61,7 @@ public async Task<ApiResponse<TData>> GetAsync<TData>(Uri uri, string? accessTok
5661
/// <inheritdoc />
5762
public async Task<ApiResponse<TData>> PostAsync<TData>(Uri uri, HttpContent? httpContent = null, string? accessToken = null, CancellationToken cancellationToken = default)
5863
{
59-
if (uri is null) throw new ArgumentNullException(nameof(uri));
64+
uri.HasValidBaseUri(nameof(uri), _options);
6065

6166
using var httpResponse = await SendRequestAsync(
6267
httpMethod: HttpMethod.Post,
@@ -74,7 +79,7 @@ public async Task<ApiResponse<TData>> PostAsync<TData>(Uri uri, HttpContent? htt
7479
/// <inheritdoc />
7580
public async Task<ApiResponse<TData>> PostAsync<TData>(Uri uri, object? request = null, string? idempotencyKey = null, string? accessToken = null, SigningKey? signingKey = null, CancellationToken cancellationToken = default)
7681
{
77-
if (uri is null) throw new ArgumentNullException(nameof(uri));
82+
uri.HasValidBaseUri(nameof(uri), _options);
7883

7984
using var httpResponse = await SendJsonRequestAsync(
8085
httpMethod: HttpMethod.Post,
@@ -91,7 +96,7 @@ public async Task<ApiResponse<TData>> PostAsync<TData>(Uri uri, object? request
9196

9297
public async Task<ApiResponse> PostAsync(Uri uri, HttpContent? httpContent = null, string? accessToken = null, CancellationToken cancellationToken = default)
9398
{
94-
if (uri is null) throw new ArgumentNullException(nameof(uri));
99+
uri.HasValidBaseUri(nameof(uri), _options);
95100

96101
using var httpResponse = await SendRequestAsync(
97102
httpMethod: HttpMethod.Post,
@@ -108,7 +113,7 @@ public async Task<ApiResponse> PostAsync(Uri uri, HttpContent? httpContent = nul
108113

109114
public async Task<ApiResponse> PostAsync(Uri uri, object? request = null, string? idempotencyKey = null, string? accessToken = null, SigningKey? signingKey = null, CancellationToken cancellationToken = default)
110115
{
111-
if (uri is null) throw new ArgumentNullException(nameof(uri));
116+
uri.HasValidBaseUri(nameof(uri), _options);
112117

113118
using var httpResponse = await SendJsonRequestAsync(
114119
httpMethod: HttpMethod.Post,

src/TrueLayer/Auth/AuthApi.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,13 @@
33
using System.Net.Http;
44
using System.Threading;
55
using System.Threading.Tasks;
6+
using TrueLayer.Common;
7+
using TrueLayer.Extensions;
68

79
namespace TrueLayer.Auth
810
{
911
internal class AuthApi : IAuthApi
1012
{
11-
internal const string ProdUrl = "https://auth.truelayer.com/";
12-
internal const string SandboxUrl = "https://auth.truelayer-sandbox.com/";
13-
1413
private readonly IApiClient _apiClient;
1514
private readonly TrueLayerOptions _options;
1615
private readonly Uri _baseUri;
@@ -20,8 +19,11 @@ public AuthApi(IApiClient apiClient, TrueLayerOptions options)
2019
_apiClient = apiClient.NotNull(nameof(apiClient));
2120
_options = options.NotNull(nameof(options));
2221

23-
_baseUri = options.Auth?.Uri ??
24-
new Uri((options.UseSandbox ?? true) ? SandboxUrl : ProdUrl);
22+
var baseUri = (options.UseSandbox ?? true)
23+
? TrueLayerBaseUris.SandboxAuthBaseUri
24+
: TrueLayerBaseUris.ProdAuthBaseUri;
25+
26+
_baseUri = options.Auth?.Uri ?? baseUri;
2527
}
2628

2729
/// <inheritdoc />
@@ -42,7 +44,7 @@ public async ValueTask<ApiResponse<GetAuthTokenResponse>> GetAuthToken(GetAuthTo
4244
}
4345

4446
return await _apiClient.PostAsync<GetAuthTokenResponse>(
45-
new Uri(_baseUri, "connect/token"), new FormUrlEncodedContent(values), null, cancellationToken);
47+
_baseUri.Append("connect/token"), new FormUrlEncodedContent(values), null, cancellationToken);
4648
}
4749
}
4850
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System;
2+
3+
namespace TrueLayer.Common;
4+
5+
internal static class TrueLayerBaseUris
6+
{
7+
internal static readonly Uri ProdApiBaseUri = new("https://api.truelayer.com/");
8+
internal static readonly Uri SandboxApiBaseUri = new("https://api.truelayer-sandbox.com/");
9+
internal static readonly Uri ProdAuthBaseUri = new("https://auth.truelayer.com/");
10+
internal static readonly Uri SandboxAuthBaseUri = new("https://auth.truelayer-sandbox.com/");
11+
internal static readonly Uri ProdHppBaseUri = new("https://payment.truelayer.com/");
12+
internal static readonly Uri SandboxHppBaseUri = new("https://payment.truelayer-sandbox.com/");
13+
}

src/TrueLayer/Extensions/UriExtensions.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Linq;
3+
using System.Text;
34
using System.Text.Json;
45
using TrueLayer.Serialization;
56

@@ -9,8 +10,8 @@ public static class UriExtensions
910
{
1011
public static Uri Append(this Uri uri, params string[] segments)
1112
{
12-
string newUri = string.Join("/", new[] { uri.AbsoluteUri.TrimEnd('/') }
13-
.Concat(segments.Select(s => s.Trim('/'))));
13+
string newUri = string.Join("/", new[] { uri.AbsoluteUri.TrimEnd('/').Replace("\\", string.Empty) }
14+
.Concat(segments.Select(s => s.Replace("\\", string.Empty).Trim('/'))));
1415
return new Uri(newUri);
1516
}
1617

src/TrueLayer/Guard.cs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Diagnostics;
33
using System.Diagnostics.CodeAnalysis;
4+
using TrueLayer.Common;
45

56
namespace TrueLayer
67
{
@@ -87,5 +88,92 @@ public static T GreaterThan<T>([NotNull] this T value, T greaterThan, string par
8788

8889
return value;
8990
}
91+
92+
/// <summary>
93+
/// Validates that the provided <paramref name="value"/> is not an URL
94+
/// </summary>
95+
/// <param name="value">The value to validate</param>
96+
/// <param name="name">The name of the argument</param>
97+
/// <returns>The value of <paramref name="value"/> if it is not an URL</returns>
98+
/// <exception cref="ArgumentException">Thrown when the value is an URL</exception>
99+
/// <example>
100+
/// <code>
101+
/// _id = id.NotAUrl(nameof(id));
102+
/// </code>
103+
/// </example>
104+
[DebuggerStepThrough]
105+
public static string? NotAUrl(this string? value, string name)
106+
=> value is not null
107+
&& (value.Contains(' ')
108+
|| Uri.IsWellFormedUriString(value, UriKind.Absolute)
109+
|| value.StartsWith('\\')
110+
|| value.Contains('/')
111+
|| value.Contains('.'))
112+
? throw new ArgumentException("Value is malformed", name)
113+
: value;
114+
115+
/// <summary>
116+
/// Validate that the provided URI one of the configured (from the options) URIs as base address, or one of the TrueLayer ones based on the environment used.
117+
/// </summary>
118+
/// <param name="value">The value to validate</param>
119+
/// <param name="name">The name of the argument</param>
120+
/// <param name="options">The <see cref="TrueLayerOptions"/> that contain the custom configured URIs</param>
121+
/// <returns>The value of <paramref name="value"/> if it is valid</returns>
122+
/// <exception cref="ArgumentException">Thrown when the value is not valid</exception>
123+
/// <example>
124+
/// <code>
125+
/// _uri = uri.HasValidBaseUri(nameof(_uri), options);
126+
/// </code>
127+
/// </example>
128+
internal static Uri? HasValidBaseUri(this Uri? value, string name, TrueLayerOptions options)
129+
{
130+
value.NotNull(name);
131+
const string errorMsg = "The URI must be a valid TrueLayer API URI one of those configured in the settings.";
132+
bool result = value.IsLoopback // is localhost?
133+
|| ((options.Payments?.Uri is not null) && options.Payments!.Uri.IsBaseOf(value))
134+
|| ((options.Auth?.Uri is not null) && options.Auth!.Uri.IsBaseOf(value))
135+
|| ((options.Payments?.HppUri is not null) && options.Payments!.HppUri.IsBaseOf(value));
136+
137+
if (options.UseSandbox == true)
138+
{
139+
result = result
140+
|| TrueLayerBaseUris.SandboxAuthBaseUri.IsBaseOf(value)
141+
|| TrueLayerBaseUris.SandboxApiBaseUri.IsBaseOf(value)
142+
|| TrueLayerBaseUris.SandboxHppBaseUri.IsBaseOf(value);
143+
}
144+
else
145+
{
146+
result = result
147+
|| TrueLayerBaseUris.ProdAuthBaseUri.IsBaseOf(value)
148+
|| TrueLayerBaseUris.ProdApiBaseUri.IsBaseOf(value)
149+
|| TrueLayerBaseUris.ProdHppBaseUri.IsBaseOf(value);
150+
}
151+
152+
result.ThrowIfFalse(name, errorMsg);
153+
return value;
154+
}
155+
156+
/// <summary>
157+
/// Validate that the provided value is not false
158+
/// </summary>
159+
/// <param name="value">The value to validate</param>
160+
/// <param name="name">The name of the argument</param>
161+
/// <param name="message">The message that needs to be assigned to the exception</param>
162+
/// <returns>The value of <paramref name="value"/> if not false</returns>
163+
/// <exception cref="ArgumentException">Thrown when the value is false</exception>
164+
/// <example>
165+
/// <code>
166+
/// _value = value.ThrowIfFalse(nameof(_value), "The value cannot be false");
167+
/// </code>
168+
/// </example>
169+
private static bool ThrowIfFalse(this bool value, string name, string message)
170+
{
171+
if (!value)
172+
{
173+
throw new ArgumentException(message, name);
174+
}
175+
176+
return value;
177+
}
90178
}
91179
}

src/TrueLayer/Mandates/IMandatesApi.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,13 @@ Task<ApiResponse<GetConstraintsResponse>> GetMandateConstraints(
122122
/// <summary>
123123
/// Revoke mandate
124124
/// </summary>
125-
/// <param name="id">The id of the mandate</param>
125+
/// <param name="mandateId">The id of the mandate</param>
126126
/// <param name="idempotencyKey">
127127
/// An idempotency key to allow safe retrying without the operation being performed multiple times.
128128
/// The value should be unique for each operation, e.g. a UUID, with the same key being sent on a retry of the same request.
129129
/// </param>
130130
/// <param name="cancellationToken">The cancellation token to cancel the operation</param>
131131
/// <returns>An API response that includes the payment details if successful, otherwise problem details</returns>
132-
Task<ApiResponse> RevokeMandate(string id, string idempotencyKey, MandateType mandateType, CancellationToken cancellationToken = default);
132+
Task<ApiResponse> RevokeMandate(string mandateId, string idempotencyKey, MandateType mandateType, CancellationToken cancellationToken = default);
133133
}
134134
}

0 commit comments

Comments
 (0)