| 1 |
<?php |
| 2 |
|
| 3 |
if (!defined('ABSPATH')) { |
| 4 |
exit; |
| 5 |
} |
| 6 |
|
| 7 |
/** |
| 8 |
* Masks the credential-bearing values that a request carries, in text that |
| 9 |
* names them: Authorization and API-key headers, cookie headers and |
| 10 |
* $_COOKIE assignments, sensitive form / JSON body fields, and WordPress |
| 11 |
* nonces. |
| 12 |
* |
| 13 |
* Every pattern here is anchored on a literal field name, so the value to |
| 14 |
* mask is never in doubt and the replacement is a constant. That is the whole |
| 15 |
* difference from ABJ_404_Solution_PiiRedactor, which has to RECOGNIZE |
| 16 |
* unlabeled shapes (an address, an IP, a path) in free-form text and has |
| 17 |
* repeatedly over-matched ordinary diagnostics while doing it. Keeping the |
| 18 |
* two apart keeps the risky half small and lets a caller that only has a |
| 19 |
* request dump to scrub run this without the shape-recognition passes. |
| 20 |
* |
| 21 |
* Ordering note: run this BEFORE the shape-recognition passes. A masked |
| 22 |
* value is inert to them ('[REDACTED]' and 'nonce-a1b2c3d4' match nothing |
| 23 |
* downstream), whereas a header value left in the clear can be misread as |
| 24 |
* an address or a token. |
| 25 |
*/ |
| 26 |
final class ABJ_404_Solution_RequestCredentialRedactor { |
| 27 |
|
| 28 |
/** |
| 29 |
* Field names whose value is a credential wherever it appears -- as a |
| 30 |
* query/form pair, as a PHP superglobal assignment, or as a JSON member. |
| 31 |
*/ |
| 32 |
const SENSITIVE_FIELD_NAMES = 'password|passwd|pwd|secret|credit_card|card_number|cvv|ssn' |
| 33 |
. '|api_key|private_key|access_token|refresh_token'; |
| 34 |
|
| 35 |
/** |
| 36 |
* @param string $text |
| 37 |
* @return string the text with every labeled credential value replaced |
| 38 |
*/ |
| 39 |
public static function redact(string $text): string { |
| 40 |
$text = self::redactAuthorizationHeaders($text); |
| 41 |
$text = self::redactCookieValues($text); |
| 42 |
$text = self::redactSensitiveFormFields($text); |
| 43 |
|
| 44 |
return self::redactNonces($text); |
| 45 |
} |
| 46 |
|
| 47 |
/** @param string $text @return string */ |
| 48 |
private static function redactAuthorizationHeaders(string $text): string { |
| 49 |
$text = preg_replace( |
| 50 |
'/\b(Authorization:\s*)(Bearer|Basic|Digest|Token)\s+\S+/i', |
| 51 |
'$1$2 [REDACTED]', |
| 52 |
$text |
| 53 |
) ?? $text; |
| 54 |
|
| 55 |
$text = preg_replace( |
| 56 |
'/\b(X-API-Key|X-Auth-Token|X-Access-Token):\s*\S+/i', |
| 57 |
'$1: [REDACTED]', |
| 58 |
$text |
| 59 |
) ?? $text; |
| 60 |
|
| 61 |
return $text; |
| 62 |
} |
| 63 |
|
| 64 |
/** @param string $text @return string */ |
| 65 |
private static function redactCookieValues(string $text): string { |
| 66 |
$text = preg_replace( |
| 67 |
'/\b(Cookie|Set-Cookie):\s*\S[^\r\n]*/i', |
| 68 |
'$1: [REDACTED]', |
| 69 |
$text |
| 70 |
) ?? $text; |
| 71 |
|
| 72 |
$text = preg_replace( |
| 73 |
'/(\$_COOKIE\s*\[\s*[\'"][^\'"]*[\'"]\s*\])\s*=\s*[\'"][^\'"]*[\'"]/i', |
| 74 |
'$1 = \'[REDACTED]\'', |
| 75 |
$text |
| 76 |
) ?? $text; |
| 77 |
|
| 78 |
return $text; |
| 79 |
} |
| 80 |
|
| 81 |
/** @param string $text @return string */ |
| 82 |
private static function redactSensitiveFormFields(string $text): string { |
| 83 |
$sensitiveKeys = self::SENSITIVE_FIELD_NAMES; |
| 84 |
|
| 85 |
$text = preg_replace( |
| 86 |
'/\b(' . $sensitiveKeys . ')\s*=\s*(?:([\'"])[^\'"]*\2|\S+)/i', |
| 87 |
'$1=[REDACTED]', |
| 88 |
$text |
| 89 |
) ?? $text; |
| 90 |
|
| 91 |
$text = preg_replace( |
| 92 |
'/(\$_(?:POST|GET|REQUEST)\s*\[\s*[\'"](?:' . $sensitiveKeys . ')[\'"]\s*\])\s*(?:=\s*[\'"][^\'"]*[\'"])?/i', |
| 93 |
'$1=[REDACTED]', |
| 94 |
$text |
| 95 |
) ?? $text; |
| 96 |
|
| 97 |
$text = preg_replace( |
| 98 |
'/"(' . $sensitiveKeys . ')"\s*:\s*"[^"]*"/i', |
| 99 |
'"$1":"[REDACTED]"', |
| 100 |
$text |
| 101 |
) ?? $text; |
| 102 |
|
| 103 |
return $text; |
| 104 |
} |
| 105 |
|
| 106 |
/** |
| 107 |
* @param string $text @return string |
| 108 |
* |
| 109 |
* A nonce is hashed rather than blanked so that the same nonce can be |
| 110 |
* recognized across log lines (it is the key evidence in a "nonce |
| 111 |
* mismatch" report) without the value itself being reusable. |
| 112 |
*/ |
| 113 |
private static function redactNonces(string $text): string { |
| 114 |
return preg_replace_callback( |
| 115 |
'/_wpnonce=([A-Za-z0-9]+)/', |
| 116 |
function ($matches) { |
| 117 |
return '_wpnonce=nonce-' . substr(md5($matches[1]), 0, 8); |
| 118 |
}, |
| 119 |
$text |
| 120 |
) ?? $text; |
| 121 |
} |
| 122 |
} |
| 123 |
|