security: Fix XSS, insecure token generation, and harden import/export (v0.4.9)

Security audit findings addressed:
- Replace jQuery .html() with safe .text() DOM construction (XSS prevention)
- Use crypto.getRandomValues() instead of Math.random() for token generation
- Add 1MB import size limit to prevent DoS via large JSON payloads
- Remove site_url from metric exports (information disclosure)
- Add import mode allowlist validation

Refactoring:
- Extract shared wp_prometheus_authenticate_request() function (DRY)
- Extract showNotice() helper in admin.js (DRY)
- Extract is_hpos_enabled() helper in Collector (DRY)

Performance:
- Optimize WooCommerce product counting with paginate COUNT query

Housekeeping:
- Add missing options to Installer::uninstall() cleanup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 07:47:37 +01:00
parent 88ce597f1e
commit 1b1e818ff4
7 changed files with 190 additions and 178 deletions

View File

@@ -102,52 +102,12 @@ class MetricsEndpoint {
/**
* Authenticate the metrics request.
*
* Uses the shared authentication helper to avoid code duplication
* with the isolated mode handler in wp-prometheus.php.
*
* @return bool
*/
private function authenticate(): bool {
$auth_token = get_option( 'wp_prometheus_auth_token', '' );
// If no token is set, deny access.
if ( empty( $auth_token ) ) {
return false;
}
// Check for Bearer token in Authorization header.
$auth_header = $this->get_authorization_header();
if ( ! empty( $auth_header ) && preg_match( '/Bearer\s+(.*)$/i', $auth_header, $matches ) ) {
return hash_equals( $auth_token, $matches[1] );
}
// Check for token in query parameter (less secure but useful for testing).
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Auth token check.
if ( isset( $_GET['token'] ) && hash_equals( $auth_token, sanitize_text_field( wp_unslash( $_GET['token'] ) ) ) ) {
return true;
}
return false;
}
/**
* Get the Authorization header from the request.
*
* @return string
*/
private function get_authorization_header(): string {
if ( isset( $_SERVER['HTTP_AUTHORIZATION'] ) ) {
return sanitize_text_field( wp_unslash( $_SERVER['HTTP_AUTHORIZATION'] ) );
}
if ( isset( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) ) {
return sanitize_text_field( wp_unslash( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) );
}
if ( function_exists( 'apache_request_headers' ) ) {
$headers = apache_request_headers();
if ( isset( $headers['Authorization'] ) ) {
return sanitize_text_field( $headers['Authorization'] );
}
}
return '';
return wp_prometheus_authenticate_request();
}
}

View File

@@ -65,6 +65,14 @@ final class Installer {
'wp_prometheus_enabled_metrics',
'wp_prometheus_runtime_metrics',
'wp_prometheus_custom_metrics',
'wp_prometheus_isolated_mode',
'wp_prometheus_storage_adapter',
'wp_prometheus_redis_host',
'wp_prometheus_redis_port',
'wp_prometheus_redis_password',
'wp_prometheus_redis_database',
'wp_prometheus_redis_prefix',
'wp_prometheus_apcu_prefix',
);
foreach ( $options as $option ) {

View File

@@ -447,6 +447,16 @@ class Collector {
return class_exists( 'WooCommerce' );
}
/**
* Check if WooCommerce HPOS (High-Performance Order Storage) is enabled.
*
* @return bool
*/
private function is_hpos_enabled(): bool {
return class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' )
&& \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
}
/**
* Collect WooCommerce metrics.
*
@@ -498,16 +508,17 @@ class Collector {
}
}
// Count by product type (for published products only).
// Count by product type (for published products only) using count query.
foreach ( array_keys( $product_types ) as $type ) {
$args = array(
'status' => 'publish',
'type' => $type,
'limit' => -1,
'limit' => 1,
'return' => 'ids',
'paginate' => true,
);
$products = wc_get_products( $args );
$gauge->set( count( $products ), array( 'publish', $type ) );
$result = wc_get_products( $args );
$gauge->set( $result->total, array( 'publish', $type ) );
}
}
@@ -553,8 +564,7 @@ class Collector {
$currency = get_woocommerce_currency();
// Check if HPOS (High-Performance Order Storage) is enabled.
$hpos_enabled = class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' )
&& \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
$hpos_enabled = $this->is_hpos_enabled();
if ( $hpos_enabled ) {
$orders_table = $wpdb->prefix . 'wc_orders';

View File

@@ -47,6 +47,13 @@ class CustomMetricBuilder {
*/
const MAX_LABEL_VALUES = 50;
/**
* Maximum import JSON size in bytes (1 MB).
*
* @var int
*/
const MAX_IMPORT_SIZE = 1048576;
/**
* Get all custom metrics.
*
@@ -332,7 +339,6 @@ class CustomMetricBuilder {
'version' => self::EXPORT_VERSION,
'plugin_version' => WP_PROMETHEUS_VERSION,
'exported_at' => gmdate( 'c' ),
'site_url' => home_url(),
'metrics' => array_values( $metrics ),
);
@@ -348,6 +354,17 @@ class CustomMetricBuilder {
* @throws \InvalidArgumentException If JSON is invalid.
*/
public function import( string $json, string $mode = 'skip' ): array {
// Prevent DoS via excessively large imports.
if ( strlen( $json ) > self::MAX_IMPORT_SIZE ) {
throw new \InvalidArgumentException(
sprintf(
/* translators: %s: Maximum size */
__( 'Import data exceeds maximum size of %s.', 'wp-prometheus' ),
size_format( self::MAX_IMPORT_SIZE )
)
);
}
$data = json_decode( $json, true );
if ( json_last_error() !== JSON_ERROR_NONE ) {
@@ -358,6 +375,12 @@ class CustomMetricBuilder {
throw new \InvalidArgumentException( __( 'No metrics found in import file.', 'wp-prometheus' ) );
}
// Validate import mode.
$valid_modes = array( 'skip', 'overwrite', 'rename' );
if ( ! in_array( $mode, $valid_modes, true ) ) {
$mode = 'skip';
}
$result = array(
'imported' => 0,
'skipped' => 0,