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>
160 lines
4.4 KiB
PHP
160 lines
4.4 KiB
PHP
<?php
|
|
/**
|
|
* Sanitizes and masks sensitive data in validated workflow arrays.
|
|
*
|
|
* @package BreznFlow
|
|
* @since 1.0.0
|
|
*/
|
|
|
|
namespace BreznFlow\Security;
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Sanitizes and masks sensitive data in validated n8n workflow arrays.
|
|
*/
|
|
class WorkflowSanitizer {
|
|
/**
|
|
* Log of masked items accumulated during processing.
|
|
*
|
|
* @var array<int, array<string, string>>
|
|
*/
|
|
private array $mask_log = array();
|
|
|
|
/**
|
|
* Processes (sanitizes + masks) a validated workflow data array.
|
|
*
|
|
* @param array $data Validated workflow data from WorkflowValidator.
|
|
* @return array{ data: array, mask_log: array } Processed data and mask log.
|
|
*/
|
|
public function process( array $data ): array {
|
|
$this->mask_log = array();
|
|
|
|
// Pass 1: Sanitize all strings.
|
|
$sanitized = $this->sanitize_recursive( $data );
|
|
|
|
// Pass 2: Mask secrets in nodes.
|
|
if ( isset( $sanitized['nodes'] ) && is_array( $sanitized['nodes'] ) ) {
|
|
foreach ( $sanitized['nodes'] as &$node ) {
|
|
$node = $this->mask_node( $node );
|
|
}
|
|
unset( $node );
|
|
}
|
|
|
|
return array(
|
|
'data' => $sanitized,
|
|
'mask_log' => $this->mask_log,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Recursively sanitizes all string values in the data.
|
|
* jsCode is preserved as-is (displayed with esc_html(), never executed).
|
|
*
|
|
* @since 1.0.0
|
|
* @param mixed $value Value to sanitize.
|
|
* @param string $parent_key Parent array key for context.
|
|
* @return mixed Sanitized value.
|
|
*/
|
|
private function sanitize_recursive( $value, string $parent_key = '' ): mixed {
|
|
if ( is_array( $value ) ) {
|
|
$result = array();
|
|
foreach ( $value as $key => $item ) {
|
|
$result[ $key ] = $this->sanitize_recursive( $item, (string) $key );
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
if ( is_string( $value ) ) {
|
|
// Preserve jsCode as-is; it will be displayed with esc_html().
|
|
if ( 'jsCode' === $parent_key ) {
|
|
return $value;
|
|
}
|
|
return sanitize_text_field( $value );
|
|
}
|
|
|
|
if ( is_int( $value ) || is_float( $value ) ) {
|
|
return $value;
|
|
}
|
|
|
|
if ( is_bool( $value ) ) {
|
|
return $value;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Applies masking rules to a single node's parameters.
|
|
*
|
|
* @since 1.0.0
|
|
* @param array $node Single workflow node array.
|
|
* @return array Node with masked parameter values.
|
|
*/
|
|
private function mask_node( array $node ): array {
|
|
if ( ! isset( $node['parameters'] ) || ! is_array( $node['parameters'] ) ) {
|
|
return $node;
|
|
}
|
|
|
|
$node['parameters'] = $this->mask_parameters_recursive( $node['parameters'] );
|
|
return $node;
|
|
}
|
|
|
|
/**
|
|
* Recursively applies masking rules to parameter values.
|
|
*
|
|
* @since 1.0.0
|
|
* @param array $params Parameters array to process.
|
|
* @return array Parameters with sensitive values masked.
|
|
*/
|
|
private function mask_parameters_recursive( array $params ): array {
|
|
foreach ( $params as $key => &$value ) {
|
|
if ( is_string( $value ) && 'jsCode' !== $key ) {
|
|
$value = MaskingRules::apply( $value, (string) $key, $this->mask_log );
|
|
|
|
// Condition rightValue heuristic.
|
|
if ( 'rightValue' === $key && '[REDACTED]' !== $value ) {
|
|
$value = MaskingRules::apply_condition_heuristic( $value, $this->mask_log );
|
|
}
|
|
} elseif ( is_array( $value ) ) {
|
|
// {name, value} pair pattern — e.g. HTTP header/body parameters.
|
|
// If the 'name' field indicates a sensitive header, mask its 'value'.
|
|
if ( isset( $value['name'], $value['value'] ) && is_string( $value['value'] ) ) {
|
|
$this->mask_name_value_pair( $value );
|
|
}
|
|
$value = $this->mask_parameters_recursive( $value );
|
|
}
|
|
}
|
|
unset( $value );
|
|
return $params;
|
|
}
|
|
|
|
/**
|
|
* Masks the 'value' of a {name, value} pair when the name implies sensitive data.
|
|
* Covers HTTP headers like Authorization, API-Key, X-Auth-Token, etc.
|
|
*
|
|
* @param array $item Passed by reference — modifies $item['value'] in place.
|
|
*/
|
|
private function mask_name_value_pair( array &$item ): void {
|
|
if ( '[REDACTED]' === $item['value'] ) {
|
|
return;
|
|
}
|
|
|
|
$name_lower = strtolower( (string) $item['name'] );
|
|
$sensitive = array( 'authorization', 'token', 'api-key', 'apikey', 'x-api-key', 'x-auth', 'secret', 'password', 'bearer' );
|
|
|
|
foreach ( $sensitive as $keyword ) {
|
|
if ( str_contains( $name_lower, $keyword ) ) {
|
|
$this->mask_log[] = array(
|
|
'reason' => 'sensitive_header_name',
|
|
'key' => 'value',
|
|
'note' => 'Parameter name "' . esc_html( $item['name'] ) . '" indicates sensitive data.',
|
|
);
|
|
$item['value'] = '[REDACTED]';
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|