breznflow/includes/Security/MaskingRules.php
Michael fd83e4810b BreznFlow 1.0.0 — WordPress.org submission
Initial public release of BreznFlow, an n8n workflow renderer for WordPress.
Fully PHPCS-compliant (WordPress Coding Standards), security-hardened,
and ready for WordPress.org plugin review.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 11:27:36 +00:00

174 lines
4.6 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* Masking rules for sensitive data in workflow parameters.
*
* @package BreznFlow
* @since 1.0.0
*/
namespace BreznFlow\Security;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Provides rules for masking sensitive values in workflow data.
*
* @since 1.0.0
*/
class MaskingRules {
/** URL query param pattern for sensitive keys. */
const URL_PARAM_PATTERN = '/([?&](?:api[_-]?key|apikey|token|secret|password|access_token|auth|private_key|client_secret)=)[^&\s#]+/i';
/** Safe-list values that should never be masked (condition rightValue). */
const SAFE_CONDITION_VALUES = array(
'true',
'false',
'null',
'undefined',
'0',
'1',
'yes',
'no',
'success',
'error',
'active',
'inactive',
'enabled',
'disabled',
'pending',
'complete',
'completed',
'failed',
);
/**
* Applies all masking rules to a string value.
*
* @param string $value Raw string to inspect.
* @param string $field_key The parameter key name (for context-aware masking).
* @param array $log Passed by reference — masked items appended here.
* @return string Possibly masked value.
*/
public static function apply( string $value, string $field_key, array &$log ): string {
$value = self::mask_url_params( $value, $log );
$value = self::mask_sensitive_field( $value, $field_key, $log );
return $value;
}
/**
* Masks sensitive URL query parameters.
*
* @since 1.0.0
* @param string $value Raw string to inspect.
* @param array $log Passed by reference — masked items appended here.
* @return string Possibly masked value.
*/
private static function mask_url_params( string $value, array &$log ): string {
if ( ! str_contains( $value, '=' ) && ! str_contains( $value, '?' ) ) {
return $value;
}
$masked = preg_replace_callback(
self::URL_PARAM_PATTERN,
function ( $matches ) use ( &$log ) {
$log[] = array(
'reason' => 'url_param',
'key' => $matches[0],
'note' => 'Sensitive URL parameter value replaced.',
);
return $matches[1] . '[REDACTED]';
},
$value
);
return null !== $masked ? $masked : $value;
}
/**
* Masks values of fields with inherently sensitive names.
*
* @since 1.0.0
* @param string $value Raw string to inspect.
* @param string $field_key The parameter key name.
* @param array $log Passed by reference — masked items appended here.
* @return string Possibly masked value.
*/
private static function mask_sensitive_field( string $value, string $field_key, array &$log ): string {
$sensitive_keys = array(
'api_key',
'apikey',
'token',
'secret',
'password',
'access_token',
'auth',
'private_key',
'client_secret',
'apiKey',
'accessToken',
'clientSecret',
'privateKey',
);
if ( in_array( $field_key, $sensitive_keys, true ) && '' !== $value && '[REDACTED]' !== $value ) {
$log[] = array(
'reason' => 'sensitive_field_name',
'key' => $field_key,
'note' => 'Field name indicates sensitive data.',
);
return '[REDACTED]';
}
return $value;
}
/**
* Applies condition rightValue heuristic masking.
* Used specifically for condition node parameter values.
*
* @since 1.0.0
* @param string $value Raw condition value.
* @param array $log Passed by reference — masked items appended here.
* @return string Possibly masked value.
*/
public static function apply_condition_heuristic( string $value, array &$log ): string {
$len = strlen( $value );
// Must be 8512 chars.
if ( $len < 8 || $len > 512 ) {
return $value;
}
// Safe-list check.
if ( in_array( strtolower( $value ), self::SAFE_CONDITION_VALUES, true ) ) {
return $value;
}
// n8n expression check.
if ( str_starts_with( $value, '={{' ) && str_ends_with( $value, '}}' ) ) {
return $value;
}
// ISO date check.
if ( preg_match( '/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2})?/', $value ) ) {
return $value;
}
// Entropy check: UUID-shaped, or mixed case+digits, or long with no spaces.
$is_uuid = (bool) preg_match( '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $value );
$is_complex = (bool) preg_match( '/[A-Z]/', $value )
&& (bool) preg_match( '/[a-z]/', $value )
&& (bool) preg_match( '/[0-9]/', $value );
$is_long_no_space = $len > 20 && ! str_contains( $value, ' ' );
if ( $is_uuid || $is_complex || $is_long_no_space ) {
$log[] = array(
'reason' => 'condition_heuristic',
'key' => 'rightValue',
'note' => 'Value matches entropy heuristic for potential secret.',
);
return '[REDACTED]';
}
return $value;
}
}