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
82 changes: 41 additions & 41 deletions LICENSE-COMMERCIAL.txt

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/net/authorize/api/constants/ANetEnvironment.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

class ANetEnvironment
{
const CUSTOM = "http://wwww.myendpoint.com";
const CUSTOM = "https://custom.endpoint.example";
const SANDBOX = "https://apitest.authorize.net";
const PRODUCTION = "https://api2.authorize.net";

Expand Down
69 changes: 29 additions & 40 deletions lib/net/authorize/util/ANetSensitiveFields.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,58 +14,47 @@ class ANetSensitiveFields
private static function fetchFromConfigFiles(){
if(!class_exists(ANET_SENSITIVE_DATE_CONFIG_CLASS))
exit("Class (".ANET_SENSITIVE_DATE_CONFIG_CLASS.") doesn't exist; can't deserialize json; can't log. Exiting.");


$userConfigFilePath = ANET_SENSITIVE_XMLTAGS_JSON_FILE;
$presentUserConfigFile = file_exists($userConfigFilePath);

$configFilePath = dirname(__FILE__) . "/" . ANET_SENSITIVE_XMLTAGS_JSON_FILE;
$useDefaultConfigFile = !$presentUserConfigFile;

if ($presentUserConfigFile) { //client config for tags
//read list of tags (and associated regex-patterns and replacements) from .json file
try{
$jsonFileData=file_get_contents($userConfigFilePath);
$sensitiveDataConfig = json_decode($jsonFileData);

$sensitiveTags = $sensitiveDataConfig->sensitiveTags;
self::$sensitiveStringRegexes = $sensitiveDataConfig->sensitiveStringRegexes;
}

catch(Exception $e){
$useDefaultConfigFile = true;
exit ("ERROR deserializing json from : " . $userConfigFilePath . "; Exception : " . $e->getMessage());
}

if(!file_exists($configFilePath)){
exit("ERROR: No config file: " . $configFilePath);
}

if ($useDefaultConfigFile) { //default sdk config for tags
if(!file_exists($configFilePath)){
exit("ERROR: No config file: " . $configFilePath);
}

//read list of tags (and associated regex-patterns and replacements) from .json file
try{
$jsonFileData=file_get_contents($configFilePath);

try{
$jsonFileData = file_get_contents($configFilePath);
$sensitiveDataConfig = json_decode($jsonFileData);

$sensitiveTags = $sensitiveDataConfig->sensitiveTags;
self::$sensitiveStringRegexes = $sensitiveDataConfig->sensitiveStringRegexes;
}

catch(Exception $e){
exit( "ERROR deserializing json from : " . $configFilePath . "; Exception : " . $e->getMessage());
}
}

//Check for disableMask flag in case of client json.
catch(\Exception $e){
exit("ERROR deserializing json from : " . $configFilePath . "; Exception : " . $e->getMessage());
}

self::$applySensitiveTags = array();
foreach($sensitiveTags as $sensitiveTag){
if($sensitiveTag->disableMask){
//skip masking continue;
continue;
}

if(trim($sensitiveTag->pattern)) {
if(@preg_match('/' . $sensitiveTag->pattern . '/u', '') === false) {
$sensitiveTag->pattern = "";
}
}
else{
array_push(self::$applySensitiveTags,$sensitiveTag);

array_push(self::$applySensitiveTags, $sensitiveTag);
}

if(is_array(self::$sensitiveStringRegexes)) {
$validatedRegexes = array();
foreach(self::$sensitiveStringRegexes as $regex) {
if(@preg_match('/' . $regex . '/u', '') !== false) {
$validatedRegexes[] = $regex;
}
}
self::$sensitiveStringRegexes = $validatedRegexes;
}
}

Expand Down
129 changes: 82 additions & 47 deletions lib/net/authorize/util/AuthorizedNetSensitiveTagsConfig.json
Original file line number Diff line number Diff line change
@@ -1,49 +1,84 @@
{
"sensitiveTags": [
{
"tagName": "cardCode",
"pattern": "",
"replacement": "",
"disableMask": false
},
{
"tagName": "cardNumber",
"pattern": "(\\p{N}+)(\\p{N}{4})",
"replacement": "xxxx-$2",
"disableMask": false
},
{
"tagName": "expirationDate",
"pattern": "",
"replacement": "",
"disableMask": false
},
{
"tagName": "accountNumber",
"pattern": "(\\p{N}+)(\\p{N}{4})",
"replacement": "xxxx-$2",
"disableMask": false
},
{
"tagName": "nameOnAccount",
"pattern": "",
"replacement": "",
"disableMask": false
},
{
"tagName": "transactionKey",
"pattern": "",
"replacement": "",
"disableMask": false
}
],
"sensitiveStringRegexes": [
"4\\p{N}{3}([\\ \\-]?)\\p{N}{4}\\1\\p{N}{4}\\1\\p{N}{4}",
"4\\p{N}{3}([\\ \\-]?)(?:\\p{N}{4}\\1){2}\\p{N}(?:\\p{N}{3})?",
"5[1-5]\\p{N}{2}([\\ \\-]?)\\p{N}{4}\\1\\p{N}{4}\\1\\p{N}{4}",
"6(?:011|22(?:1(?=[\\ \\-]?(?:2[6-9]|[3-9]))|[2-8]|9(?=[\\ \\-]?(?:[01]|2[0-5])))|4[4-9]\\p{N}|5\\p{N}\\p{N})([\\ \\-]?)\\p{N}{4}\\1\\p{N}{4}\\1\\p{N}{4}",
"35(?:2[89]|[3-8]\\p{N})([\\ \\-]?)\\p{N}{4}\\1\\p{N}{4}\\1\\p{N}{4}",
"3[47]\\p{N}\\p{N}([\\ \\-]?)\\p{N}{6}\\1\\p{N}{5}"
]
"sensitiveTags": [
{
"tagName": "cardCode",
"pattern": "",
"replacement": "",
"disableMask": false
},
{
"tagName": "cardNumber",
"pattern": "(\\p{N}+)(\\p{N}{4})",
"replacement": "xxxx-$2",
"disableMask": false
},
{
"tagName": "expirationDate",
"pattern": "",
"replacement": "",
"disableMask": false
},
{
"tagName": "accountNumber",
"pattern": "(\\p{N}+)(\\p{N}{4})",
"replacement": "xxxx-$2",
"disableMask": false
},
{
"tagName": "nameOnAccount",
"pattern": "",
"replacement": "",
"disableMask": false
},
{
"tagName": "transactionKey",
"pattern": "",
"replacement": "",
"disableMask": false
},
{
"tagName": "password",
"pattern": "",
"replacement": "",
"disableMask": false
},
{
"tagName": "sessionToken",
"pattern": "",
"replacement": "",
"disableMask": false
},
{
"tagName": "fingerPrint",
"pattern": "",
"replacement": "",
"disableMask": false
},
{
"tagName": "clientKey",
"pattern": "",
"replacement": "",
"disableMask": false
},
{
"tagName": "accessToken",
"pattern": "",
"replacement": "",
"disableMask": false
},
{
"tagName": "mobileDeviceId",
"pattern": "",
"replacement": "",
"disableMask": false
}
],
"sensitiveStringRegexes": [
"4\\p{N}{3}([\\ \\-]?)\\p{N}{4}\\1\\p{N}{4}\\1\\p{N}{4}",
"4\\p{N}{3}([\\ \\-]?)(?:\\p{N}{4}\\1){2}\\p{N}(?:\\p{N}{3})?",
"5[1-5]\\p{N}{2}([\\ \\-]?)\\p{N}{4}\\1\\p{N}{4}\\1\\p{N}{4}",
"6(?:011|22(?:1(?=[\\ \\-]?(?:2[6-9]|[3-9]))|[2-8]|9(?=[\\ \\-]?(?:[01]|2[0-5])))|4[4-9]\\p{N}|5\\p{N}\\p{N})([\\ \\-]?)\\p{N}{4}\\1\\p{N}{4}\\1\\p{N}{4}",
"35(?:2[89]|[3-8]\\p{N})([\\ \\-]?)\\p{N}{4}\\1\\p{N}{4}\\1\\p{N}{4}",
"3[47]\\p{N}\\p{N}([\\ \\-]?)\\p{N}{6}\\1\\p{N}{5}"
]
}

20 changes: 17 additions & 3 deletions lib/net/authorize/util/HttpClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,16 +65,27 @@ public function _sendRequest($xmlRequest)
$xmlResponse = "";

$post_url = $this->_getPostUrl();

// SECURITY: Enforce HTTPS — reject any non-TLS endpoint to prevent
// credential/cardholder-data transmission in cleartext (PCI DSS 4.1).
if (strpos($post_url, 'https://') !== 0) {
$this->logger->error("SECURITY: Refusing to send request — endpoint URL does not use HTTPS: " . parse_url($post_url, PHP_URL_SCHEME) . "://***");
return false;
}

$curl_request = curl_init($post_url);
curl_setopt($curl_request, CURLOPT_POSTFIELDS, $xmlRequest);
curl_setopt($curl_request, CURLOPT_HEADER, 0);
curl_setopt($curl_request, CURLOPT_TIMEOUT, 45);
curl_setopt($curl_request, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl_request, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($curl_request, CURLOPT_SSL_VERIFYPEER, true);

$this->logger->info(sprintf(" Url: %s", $post_url));
// Do not log requests that could contain CC info.
$this->logger->info(sprintf("Request to AnetApi: \n%s", $xmlRequest));
// SECURITY: Do not log raw request body — it contains sensitive payment data
// (cardNumber, cardCode, transactionKey, accountNumber, expirationDate).
// Log only payload length for debugging purposes.
$this->logger->info(sprintf("Request to AnetApi: payloadLength=%d", strlen($xmlRequest)));

if ($this->VERIFY_PEER) {
curl_setopt($curl_request, CURLOPT_CAINFO, dirname(dirname(__FILE__)) . '/../../ssl/cert.pem');
Expand All @@ -93,7 +104,10 @@ public function _sendRequest($xmlRequest)
{
$this->logger->info("Sending http request via Curl");
$xmlResponse = curl_exec($curl_request);
$this->logger->info("Response from AnetApi: $xmlResponse");
// SECURITY: Do not log raw response body — it may contain sensitive payment data.
// Log only response length and HTTP status for debugging purposes.
$httpCode = curl_getinfo($curl_request, CURLINFO_HTTP_CODE);
$this->logger->info(sprintf("Response from AnetApi: httpStatus=%d, responseLength=%d", $httpCode, strlen($xmlResponse ?: '')));

} catch (\Exception $ex)
{
Expand Down
51 changes: 46 additions & 5 deletions lib/net/authorize/util/Log.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ class Log
*/
private function addDelimiterFwdSlash($regexPattern)
{
return '/'.$regexPattern.'/u';
// SECURITY: 's' (dotall) flag ensures sensitive values spanning newlines are matched.
// 'u' enables Unicode mode for \p{N} patterns.
return '/'.$regexPattern.'/su';
}

/**
Expand All @@ -65,7 +67,7 @@ private function maskSensitiveXmlString($rawString){
if(trim($sensitiveTag->pattern)) {
$inputPattern = $sensitiveTag->pattern;
}
$pattern = "<" . $tag . ">(?:.*)". $inputPattern ."(?:.*)<\/" . $tag . ">";
$pattern = "<" . $tag . ">(?:.*?)". $inputPattern ."(?:.*?)<\/" . $tag . ">";
$pattern = $this->addDelimiterFwdSlash($pattern);

if(trim($sensitiveTag->replacement)) {
Expand All @@ -77,6 +79,38 @@ private function maskSensitiveXmlString($rawString){
$replacements[$i] = $replacement;
}
$maskedString = preg_replace($patterns, $replacements, $rawString);
if ($maskedString === null) {
$maskedString = '[REDACTED - XML masking failed due to PCRE error]';
}
return $maskedString;
}

/**
* Takes a JSON string and masks sensitive fields by key name.
* Handles the actual wire format used by the SDK (json_encode payloads).
*
* @param string $rawString The JSON string.
*
* @return string The string after masking sensitive JSON key values.
*/
private function maskSensitiveJsonString($rawString){
$patterns = array();
$replacements = array();

foreach ($this->sensitiveXmlTags as $i => $sensitiveTag){
$key = preg_quote($sensitiveTag->tagName, '/');
$inputReplacement = "xxxx";

$pattern = '/"' . $key . '"\s*:\s*"((?:[^"\\\\]|\\\\.)*)"/su';
$replacement = '"' . $sensitiveTag->tagName . '":"' . $inputReplacement . '"';

$patterns[$i] = $pattern;
$replacements[$i] = $replacement;
}
$maskedString = preg_replace($patterns, $replacements, $rawString);
if ($maskedString === null) {
$maskedString = '[REDACTED - JSON masking failed due to PCRE error]';
}
return $maskedString;
}

Expand All @@ -100,6 +134,9 @@ private function maskCreditCards($rawString){
$replacements[$i] = $replacement;
}
$maskedString = preg_replace($patterns, $replacements, $rawString);
if ($maskedString === null) {
$maskedString = '[REDACTED - credit card masking failed due to PCRE error]';
}
return $maskedString;
}

Expand Down Expand Up @@ -238,12 +275,16 @@ private function getMasked($raw)
else { //$messageType == "string")
$primtiveTypeAsString = strval($raw);

$maskedXml = $primtiveTypeAsString;
// SECURITY: Apply all masking layers — XML tags, JSON keys, and credit card patterns.
// The SDK wire format is JSON (ApiOperationBase sends json_encode), so JSON masking
// is the primary defense. XML masking is retained for backward compatibility.
$masked = $primtiveTypeAsString;
if($messageType == "string") {
$maskedXml = $this->maskSensitiveXmlString($primtiveTypeAsString);
$masked = $this->maskSensitiveXmlString($masked);
$masked = $this->maskSensitiveJsonString($masked);
}
//mask credit card numbers
$message = $this->maskCreditCards($maskedXml);
$message = $this->maskCreditCards($masked);
}
return $message;
}
Expand Down
Loading