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>
174 lines
4.6 KiB
PHP
174 lines
4.6 KiB
PHP
<?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 8–512 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;
|
||
}
|
||
}
|