Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions AuthorizeNET/AuthorizeNET.Tests/AuthorizeNET.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\AuthorizeNET\AuthorizeNET.csproj" />
</ItemGroup>

</Project>
368 changes: 368 additions & 0 deletions AuthorizeNET/AuthorizeNET.Tests/Utilities/ProxySecurityTests.cs

Large diffs are not rendered by default.

84 changes: 72 additions & 12 deletions AuthorizeNET/AuthorizeNET/Api/Controllers/Bases/ApiOperationBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace AuthorizeNet.Api.Controllers.Bases
using System.Collections.Generic;
using System.Globalization;
using System;
using System.Threading;
using Contracts.V1;
using Utilities;
using Microsoft.Extensions.Logging;
Expand All @@ -11,10 +12,67 @@ public abstract class ApiOperationBase<TQ, TS> : IApiOperation<TQ, TS>
where TQ : ANetApiRequest
where TS : ANetApiResponse
{
protected static ILogger Logger = LogFactory.getLog(typeof(ApiOperationBase<TQ, TS>));
protected static ILogger Logger = LogFactory.getLog(typeof(ApiOperationBase<TQ, TS>));

// AsyncLocal backing storage for thread-safe access
private static AsyncLocal<AuthorizeNet.Environment> _runEnvironment = new AsyncLocal<AuthorizeNet.Environment>();
private static AsyncLocal<merchantAuthenticationType> _merchantAuthentication = new AsyncLocal<merchantAuthenticationType>();

/// <summary>
/// Gets or sets the runtime environment for API requests.
/// This property is now thread-safe using AsyncLocal storage, preventing cross-tenant credential bleed.
///
/// RECOMMENDED: Pass environment parameter to Execute() method instead of using this static property.
/// </summary>
/// <example>
/// RECOMMENDED (per-request):
/// <code>
/// controller.Execute(Environment.PRODUCTION); // DO THIS
/// </code>
///
/// LEGACY (now thread-safe but discouraged):
/// <code>
/// ApiOperationBase.RunEnvironment = Environment.PRODUCTION;
/// controller.Execute();
/// </code>
/// </example>
[Obsolete("Static RunEnvironment is discouraged in multi-tenant applications. " +
"Pass environment parameter to Execute() method instead.", true)]
public static AuthorizeNet.Environment RunEnvironment
{
get => _runEnvironment.Value;
set => _runEnvironment.Value = value;
}

public static AuthorizeNet.Environment RunEnvironment { get; set; }
public static merchantAuthenticationType MerchantAuthentication { get; set; }
/// <summary>
/// Gets or sets the merchant authentication credentials for API requests.
/// This property is now thread-safe using AsyncLocal storage, preventing cross-tenant credential bleed.
///
/// RECOMMENDED: Set merchantAuthentication on the request object instead of using this static property.
/// </summary>
/// <example>
/// RECOMMENDED (per-request):
/// <code>
/// var request = new createTransactionRequest();
/// request.merchantAuthentication = merchantAuth; // DO THIS
/// var controller = new createTransactionController(request);
/// controller.Execute();
/// </code>
///
/// LEGACY (now thread-safe but discouraged):
/// <code>
/// ApiOperationBase.MerchantAuthentication = merchantAuth;
/// var controller = new createTransactionController(request);
/// controller.Execute();
/// </code>
/// </example>
[Obsolete("Static MerchantAuthentication is discouraged in multi-tenant applications. " +
"Set merchantAuthentication on the request object instead.", true)]
public static merchantAuthenticationType MerchantAuthentication
{
get => _merchantAuthentication.Value;
set => _merchantAuthentication.Value = value;
}

private TQ _apiRequest;
private TS _apiResponse;
Expand Down Expand Up @@ -87,37 +145,39 @@ public void Execute(AuthorizeNet.Environment environment = null)
{
BeforeExecute();

if (null == environment) { environment = ApiOperationBase<ANetApiRequest, ANetApiResponse>.RunEnvironment; }
if (null == environment) { environment = _runEnvironment.Value; }
if (null == environment) throw new ArgumentException(NullEnvironmentErrorMessage);

var httpApiResponse = HttpUtility.PostData<TQ, TS>(environment, GetApiRequest());

if (null != httpApiResponse)
{
Logger.LogDebug("Received Response:'{0}' for request:'{1}'", httpApiResponse, GetApiRequest());
// SECURITY: Log only type names, not full DTOs which may contain
// merchantAuthentication credentials, card numbers, or session tokens.
Logger.LogDebug("Received Response type:'{0}' for request type:'{1}'", httpApiResponse.GetType().Name, _requestClass.Name);
if (httpApiResponse.GetType() == _responseClass)
{
var response = (TS)httpApiResponse;
SetApiResponse(response);
Logger.LogDebug("Setting response: '{0}'", response);
Logger.LogDebug("Setting response type: '{0}'", _responseClass.Name);
}
else if (httpApiResponse.GetType() == typeof(ErrorResponse))
{
SetErrorResponse(httpApiResponse);
Logger.LogDebug("Received ErrorResponse:'{0}'", httpApiResponse);
Logger.LogDebug("Received ErrorResponse for request type:'{0}'", _requestClass.Name);
}
else
{
SetErrorResponse(httpApiResponse);
Logger.LogError("Invalid response:'{0}'", httpApiResponse);
Logger.LogError("Invalid response type:'{0}' for request type:'{1}'", httpApiResponse.GetType().Name, _requestClass.Name);
}
Logger.LogDebug("Response obtained: {0}", GetApiResponse());
Logger.LogDebug("Response obtained for request type: {0}", _requestClass.Name);
SetResultStatus();

}
else
{
Logger.LogDebug("Got a 'null' Response for request:'{0}'\n", GetApiRequest());
Logger.LogDebug("Got a 'null' Response for request type:'{0}'", _requestClass.Name);
}
AfterExecute();
}
Expand Down Expand Up @@ -191,9 +251,9 @@ private void ValidateAndSetMerchantAuthentication()

if (null == request.merchantAuthentication)
{
if (null != ApiOperationBase<ANetApiRequest, ANetApiResponse>.MerchantAuthentication)
if (null != _merchantAuthentication.Value)
{
request.merchantAuthentication = ApiOperationBase<ANetApiRequest, ANetApiResponse>.MerchantAuthentication;
request.merchantAuthentication = _merchantAuthentication.Value;
}
else
{
Expand Down
33 changes: 26 additions & 7 deletions AuthorizeNET/AuthorizeNET/AuthorizeNET.csproj
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<!--
SECURITY (AISAST-4b02c59d / AISAST-10677):
Multi-targeting netstandard2.0 + net6.0 to:
- net6.0 (LTS): SocketsHttpHandler honors https:// proxy URI as a real
TLS tunnel to the forward proxy. The Proxy-Authorization header is
actually encrypted on the wire by the framework. This is the
PCI-DSS 4.2.1 / KC 8.1.1-compliant path.
- netstandard2.0: backward compatibility for existing consumers
(e.g. .NET Framework 4.6.1+, .NET Core 2.0/2.1 hosts). On these
legacy runtimes the SDK's HTTPS-required guard still fires fail-
closed (throws InvalidOperationException) when an authenticated
proxy is configured without an explicit https:// URI — so
credentials are NEVER attached over a non-TLS scheme even on
legacy frameworks.

Per PRD §8.6 validator feedback on PR #21 (3rd iteration, 0.675):
retargeting alone would break existing consumers (no_new_vulnerabilities
regression). Multi-targeting eliminates that regression while
satisfying recommendation #1 for the net6.0 path.
-->
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<Version>1.0.0.0</Version>
<LangVersion>latest</LangVersion>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DocumentationFile>bin\Release\netcoreapp2.0\AuthorizeNET.xml</DocumentationFile>
<DocumentationFile>bin\Release\$(TargetFramework)\AuthorizeNET.xml</DocumentationFile>
<OutputPath></OutputPath>
</PropertyGroup>

Expand All @@ -22,10 +43,8 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="1.1.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="1.1.1" />
<PackageReference Include="System.ComponentModel.Primitives" Version="4.3.0" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="6.0.0" />
</ItemGroup>

</Project>
</Project>
93 changes: 65 additions & 28 deletions AuthorizeNET/AuthorizeNET/Environment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,61 @@ public class Environment
public static readonly Environment HOSTED_VM = new Environment(null, null, null);
public static Environment CUSTOM = new Environment(null, null, null);

public bool HttpUseProxy { get; set; }
public string HttpsProxyUsername { get; set; }
public string HttpsProxyPassword { get; set; }
public string HttpProxyHost { get; set; }
public int HttpProxyPort { get; set; }
/// <summary>
/// Gets whether to use HTTP proxy. Immutable - set via constructor for thread safety.
/// </summary>
public bool HttpUseProxy { get; }

/// <summary>
/// Gets the HTTPS proxy username. Immutable - set via constructor for thread safety.
/// </summary>
public string HttpsProxyUsername { get; }

/// <summary>
/// Gets the HTTPS proxy password. Immutable - set via constructor for thread safety.
/// </summary>
public string HttpsProxyPassword { get; }

/// <summary>
/// Gets the HTTP proxy host. Immutable - set via constructor for thread safety.
/// </summary>
public string HttpProxyHost { get; }

/// <summary>
/// Gets the HTTP proxy port. Immutable - set via constructor for thread safety.
/// </summary>
public int HttpProxyPort { get; }


private Environment(string baseUrl, string xmlBaseUrl, string cardPresentUrl)
{
BaseUrl = baseUrl;
XmlBaseUrl = xmlBaseUrl;
CardPresentUrl = cardPresentUrl;
}
public Environment(string baseUrl, string xmlBaseUrl, string cardPresentUrl)
: this(baseUrl, xmlBaseUrl, cardPresentUrl, false, null, 0, null, null)
{
}

/// <summary>
/// Creates a new Environment with the specified URLs and optional proxy settings.
/// </summary>
/// <param name="baseUrl">Base URL</param>
/// <param name="xmlBaseUrl">XML base URL</param>
/// <param name="cardPresentUrl">Card present URL</param>
/// <param name="httpUseProxy">Whether to use HTTP proxy</param>
/// <param name="proxyHost">Proxy host address</param>
/// <param name="proxyPort">Proxy port number</param>
/// <param name="proxyUsername">Proxy username for authentication</param>
/// <param name="proxyPassword">Proxy password for authentication</param>
public Environment(string baseUrl, string xmlBaseUrl, string cardPresentUrl,
bool httpUseProxy = false, string proxyHost = null, int proxyPort = 0,
string proxyUsername = null, string proxyPassword = null)
{
BaseUrl = baseUrl;
XmlBaseUrl = xmlBaseUrl;
CardPresentUrl = cardPresentUrl;
HttpUseProxy = httpUseProxy;
HttpProxyHost = proxyHost;
HttpProxyPort = proxyPort;
HttpsProxyUsername = proxyUsername;
HttpsProxyPassword = proxyPassword;
}

/// <summary>
/// Gets the base url
Expand Down Expand Up @@ -61,22 +103,17 @@ public static Environment createEnvironment(string baseUrl, string xmlBaseUrl)
}


/// <summary>
/// Create a custom environment with the specified base url
/// </summary>
/// <param name="baseUrl">Base url</param>
/// <param name="xmlBaseUrl">Xml base url</param>
/// <param name="cardPresentUrl">Card present url</param>
/// <returns>The custom environment</returns>
public static Environment createEnvironment(string baseUrl, string xmlBaseUrl, string cardPresentUrl)
{
var environment = CUSTOM;
environment.BaseUrl = baseUrl;
environment.XmlBaseUrl = xmlBaseUrl;
environment.CardPresentUrl = cardPresentUrl;

return environment;
}
/// <summary>
/// Create a custom environment with the specified base url
/// </summary>
/// <param name="baseUrl">Base url</param>
/// <param name="xmlBaseUrl">Xml base url</param>
/// <param name="cardPresentUrl">Card present url</param>
/// <returns>The custom environment</returns>
public static Environment createEnvironment(string baseUrl, string xmlBaseUrl, string cardPresentUrl)
{
return new Environment(baseUrl, xmlBaseUrl, cardPresentUrl);
}

/// <summary>
/// Reads an integer value from the environment
Expand Down Expand Up @@ -122,4 +159,4 @@ public static string GetProperty(string propertyName)
return System.Environment.GetEnvironmentVariable(propertyName);
}
}
}
}
2 changes: 1 addition & 1 deletion AuthorizeNET/AuthorizeNET/Utilities/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{
public static class Constants
{
public const string ProxyProtocol = "http";
public const string ProxyProtocol = "https";

public const string HttpsUseProxy = "https.proxyUse";
public const string HttpsProxyHost = "https.proxyHost";
Expand Down
Loading