= 16 && ! preg_match( '/\s/', $value ); } /** * Redact secrets from a scalar string. * * @param string $value Value. */ public static function scrub_string( string $value ): string { foreach ( self::$value_patterns as $pattern ) { $value = (string) preg_replace( $pattern, self::MASK, $value ); } // DB credentials that leak through config dumps. foreach ( array( 'DB_PASSWORD', 'AUTH_KEY', 'SECURE_AUTH_KEY', 'LOGGED_IN_KEY', 'NONCE_KEY', 'AUTH_SALT', 'SECURE_AUTH_SALT', 'LOGGED_IN_SALT', 'NONCE_SALT' ) as $const ) { $value = (string) preg_replace( "/(define\s*\(\s*['\"]" . $const . "['\"]\s*,\s*)(['\"]).*?\\2/", '$1$2' . self::MASK . '$2', $value ); if ( defined( $const ) ) { $secret = (string) constant( $const ); if ( strlen( $secret ) >= 6 ) { $value = str_replace( $secret, self::MASK, $value ); } } } foreach ( array( 'DB_USER', 'DB_NAME', 'DB_HOST' ) as $const ) { $value = (string) preg_replace( "/(define\s*\(\s*['\"]" . $const . "['\"]\s*,\s*)(['\"]).*?\\2/", '$1$2' . self::MASK . '$2', $value ); } return $value; } /** * Recursively redact a structure. * * @param mixed $data Data. * @param int $depth Recursion depth guard. * @return mixed */ public static function redact( $data, int $depth = 0 ) { if ( $depth > 32 ) { return '[DEPTH_LIMIT]'; } if ( is_string( $data ) ) { return self::scrub_string( $data ); } if ( is_object( $data ) ) { $data = get_object_vars( $data ); } if ( ! is_array( $data ) ) { return $data; } $out = array(); foreach ( $data as $key => $value ) { // ACF field and group keys are ids, not secrets. if ( 'key' === $key && is_string( $value ) && preg_match( '/^(group|field|layout)_[A-Za-z0-9]{6,32}$/', $value ) ) { $out[ $key ] = $value; continue; } // On/off switches named after passwords (e.g. "disable application passwords") are not secrets. if ( is_string( $key ) && ( 0 === $value || 1 === $value || ( is_string( $value ) && preg_match( '/^(0|1|yes|no|on|off|true|false|enabled|disabled)$/i', $value ) ) ) && preg_match( '/(_enabled|_required)$|^(disable|enable)[a-z_]*$|_disable[a-z]*$|_(on|off)$/i', $key ) ) { $out[ $key ] = $value; continue; } if ( is_string( $key ) && self::is_secret_key( $key ) && ! is_bool( $value ) && null !== $value && '' !== $value && ! is_array( $value ) ) { $out[ $key ] = self::MASK; continue; } if ( is_string( $key ) && ( ( self::is_secret_key( $key ) && is_array( $value ) ) || self::is_secret_plain_key( $key, $value ) ) ) { $out[ $key ] = self::MASK; continue; } $out[ $key ] = self::redact( $value, $depth + 1 ); } return $out; } /** * Mask an email address: jane@example.com -> j***@example.com. * * @param string $email Email. */ public static function mask_email( string $email ): string { if ( ! is_email( $email ) ) { return $email; } list( $local, $domain ) = explode( '@', $email, 2 ); return substr( $local, 0, 1 ) . '***@' . $domain; } /** * Mask a phone number, keeping the last 3 digits. * * @param string $phone Phone. */ public static function mask_phone( string $phone ): string { $digits = preg_replace( '/\D/', '', $phone ); if ( strlen( (string) $digits ) < 5 ) { return $phone; } return str_repeat( '*', strlen( (string) $digits ) - 3 ) . substr( (string) $digits, -3 ); } /** * Minimize customer PII in a flat array (billing / shipping / entries). * * @param array $row Row. * @param bool $mask Whether masking is enabled. * @return array */ public static function minimize_pii( array $row, bool $mask = true ): array { if ( ! $mask ) { return self::redact( $row ); } foreach ( $row as $key => $value ) { $k = strtolower( (string) $key ); if ( is_array( $value ) ) { $row[ $key ] = self::minimize_pii( $value, $mask ); continue; } if ( ! is_string( $value ) ) { continue; } if ( false !== strpos( $k, 'email' ) ) { $row[ $key ] = self::mask_email( $value ); } elseif ( false !== strpos( $k, 'phone' ) || false !== strpos( $k, 'mobile' ) ) { $row[ $key ] = self::mask_phone( $value ); } elseif ( preg_match( '/(address_1|address_2|street|postcode|zip|ip_address|customer_ip|user_ip|birth|ssn|tax_id|vat)/', $k ) ) { $row[ $key ] = '' === $value ? '' : '[PII_MINIMIZED]'; } } return self::redact( $row ); } }