Skip to content

Commit

Permalink
Merge pull request from GHSA-67m4-qxp3-j6hh
Browse files Browse the repository at this point in the history
fix: validated that IDs in input are not valid URIs
  • Loading branch information
tl-mauro-franchi committed Jan 22, 2024
2 parents 9bf4120 + 7e0887f commit 75e436e
Show file tree
Hide file tree
Showing 16 changed files with 343 additions and 75 deletions.
17 changes: 11 additions & 6 deletions src/TrueLayer/ApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
using System.Net.Mime;
using TrueLayer.Serialization;
using System.Text.Json;
using Microsoft.Extensions.Options;
using TrueLayer.Common;
using TrueLayer.Signing;
#if NET6_0 || NET6_0_OR_GREATER
using System.Net.Http.Json;
Expand All @@ -25,20 +27,23 @@ internal class ApiClient : IApiClient
= $"truelayer-dotnet/{ReflectionUtils.GetAssemblyVersion<ITrueLayerClient>()}";

private readonly HttpClient _httpClient;
private readonly TrueLayerOptions _options;

/// <summary>
/// Creates a new <see cref="ApiClient"/> instance with the provided configuration, HTTP client factory and serializer.
/// </summary>
/// <param name="httpClient">The client used to make HTTP requests.</param>
public ApiClient(HttpClient httpClient)
/// <param name="options"></param>
public ApiClient(HttpClient httpClient, IOptions<TrueLayerOptions> options)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_options = options.Value ?? throw new ArgumentNullException(nameof(options));
}

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

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

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

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

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

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

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

using var httpResponse = await SendJsonRequestAsync(
httpMethod: HttpMethod.Post,
Expand Down
14 changes: 8 additions & 6 deletions src/TrueLayer/Auth/AuthApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using TrueLayer.Common;
using TrueLayer.Extensions;

namespace TrueLayer.Auth
{
internal class AuthApi : IAuthApi
{
internal const string ProdUrl = "https://auth.truelayer.com/";
internal const string SandboxUrl = "https://auth.truelayer-sandbox.com/";

private readonly IApiClient _apiClient;
private readonly TrueLayerOptions _options;
private readonly Uri _baseUri;
Expand All @@ -20,8 +19,11 @@ public AuthApi(IApiClient apiClient, TrueLayerOptions options)
_apiClient = apiClient.NotNull(nameof(apiClient));
_options = options.NotNull(nameof(options));

_baseUri = options.Auth?.Uri ??
new Uri((options.UseSandbox ?? true) ? SandboxUrl : ProdUrl);
var baseUri = (options.UseSandbox ?? true)
? TrueLayerBaseUris.SandboxAuthBaseUri
: TrueLayerBaseUris.ProdAuthBaseUri;

_baseUri = options.Auth?.Uri ?? baseUri;
}

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

return await _apiClient.PostAsync<GetAuthTokenResponse>(
new Uri(_baseUri, "connect/token"), new FormUrlEncodedContent(values), null, cancellationToken);
_baseUri.Append("connect/token"), new FormUrlEncodedContent(values), null, cancellationToken);
}
}
}
13 changes: 13 additions & 0 deletions src/TrueLayer/Common/TrueLayerBaseUris.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;

namespace TrueLayer.Common;

internal static class TrueLayerBaseUris
{
internal static readonly Uri ProdApiBaseUri = new("https://api.truelayer.com/");
internal static readonly Uri SandboxApiBaseUri = new("https://api.truelayer-sandbox.com/");
internal static readonly Uri ProdAuthBaseUri = new("https://auth.truelayer.com/");
internal static readonly Uri SandboxAuthBaseUri = new("https://auth.truelayer-sandbox.com/");
internal static readonly Uri ProdHppBaseUri = new("https://payment.truelayer.com/");
internal static readonly Uri SandboxHppBaseUri = new("https://payment.truelayer-sandbox.com/");
}
5 changes: 3 additions & 2 deletions src/TrueLayer/Extensions/UriExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Linq;
using System.Text;
using System.Text.Json;
using TrueLayer.Serialization;

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

Expand Down
88 changes: 88 additions & 0 deletions src/TrueLayer/Guard.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using TrueLayer.Common;

namespace TrueLayer
{
Expand Down Expand Up @@ -87,5 +88,92 @@ public static T NotNull<T>([NotNull] this T value, string name)

return value;
}

/// <summary>
/// Validates that the provided <paramref name="value"/> is not an URL
/// </summary>
/// <param name="value">The value to validate</param>
/// <param name="name">The name of the argument</param>
/// <returns>The value of <paramref name="value"/> if it is not an URL</returns>
/// <exception cref="ArgumentException">Thrown when the value is an URL</exception>
/// <example>
/// <code>
/// _id = id.NotAUrl(nameof(id));
/// </code>
/// </example>
[DebuggerStepThrough]
public static string? NotAUrl(this string? value, string name)
=> value is not null
&& (value.Contains(' ')
|| Uri.IsWellFormedUriString(value, UriKind.Absolute)
|| value.StartsWith('\\')
|| value.Contains('/')
|| value.Contains('.'))
? throw new ArgumentException("Value is malformed", name)
: value;

/// <summary>
/// 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.
/// </summary>
/// <param name="value">The value to validate</param>
/// <param name="name">The name of the argument</param>
/// <param name="options">The <see cref="TrueLayerOptions"/> that contain the custom configured URIs</param>
/// <returns>The value of <paramref name="value"/> if it is valid</returns>
/// <exception cref="ArgumentException">Thrown when the value is not valid</exception>
/// <example>
/// <code>
/// _uri = uri.HasValidBaseUri(nameof(_uri), options);
/// </code>
/// </example>
internal static Uri? HasValidBaseUri(this Uri? value, string name, TrueLayerOptions options)
{
value.NotNull(name);
const string errorMsg = "The URI must be a valid TrueLayer API URI one of those configured in the settings.";
bool result = value.IsLoopback // is localhost?
|| ((options.Payments?.Uri is not null) && options.Payments!.Uri.IsBaseOf(value))
|| ((options.Auth?.Uri is not null) && options.Auth!.Uri.IsBaseOf(value))
|| ((options.Payments?.HppUri is not null) && options.Payments!.HppUri.IsBaseOf(value));

if (options.UseSandbox == true)
{
result = result
|| TrueLayerBaseUris.SandboxAuthBaseUri.IsBaseOf(value)
|| TrueLayerBaseUris.SandboxApiBaseUri.IsBaseOf(value)
|| TrueLayerBaseUris.SandboxHppBaseUri.IsBaseOf(value);
}
else
{
result = result
|| TrueLayerBaseUris.ProdAuthBaseUri.IsBaseOf(value)
|| TrueLayerBaseUris.ProdApiBaseUri.IsBaseOf(value)
|| TrueLayerBaseUris.ProdHppBaseUri.IsBaseOf(value);
}

result.ThrowIfFalse(name, errorMsg);
return value;
}

/// <summary>
/// Validate that the provided value is not false
/// </summary>
/// <param name="value">The value to validate</param>
/// <param name="name">The name of the argument</param>
/// <param name="message">The message that needs to be assigned to the exception</param>
/// <returns>The value of <paramref name="value"/> if not false</returns>
/// <exception cref="ArgumentException">Thrown when the value is false</exception>
/// <example>
/// <code>
/// _value = value.ThrowIfFalse(nameof(_value), "The value cannot be false");
/// </code>
/// </example>
private static bool ThrowIfFalse(this bool value, string name, string message)
{
if (!value)
{
throw new ArgumentException(message, name);
}

return value;
}
}
}
4 changes: 2 additions & 2 deletions src/TrueLayer/Mandates/IMandatesApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,13 @@ public interface IMandatesApi
/// <summary>
/// Revoke mandate
/// </summary>
/// <param name="id">The id of the mandate</param>
/// <param name="mandateId">The id of the mandate</param>
/// <param name="idempotencyKey">
/// An idempotency key to allow safe retrying without the operation being performed multiple times.
/// 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.
/// </param>
/// <param name="cancellationToken">The cancellation token to cancel the operation</param>
/// <returns>An API response that includes the payment details if successful, otherwise problem details</returns>
Task<ApiResponse> RevokeMandate(string id, string idempotencyKey, MandateType mandateType, CancellationToken cancellationToken = default);
Task<ApiResponse> RevokeMandate(string mandateId, string idempotencyKey, MandateType mandateType, CancellationToken cancellationToken = default);
}
}
Loading

0 comments on commit 75e436e

Please sign in to comment.