PluginProbe
404 Solution / trunk
404 Solution vtrunk
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / feedback / RequestCredentialRedactor.php

RequestCredentialRedactor.php in 404 Solution trunk, at includes/feedback/RequestCredentialRedactor.php

123 lines 4.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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