breznflow/includes/Security/WorkflowSanitizer.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

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;
}
}
}
}