You've already forked wp-prometheus
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:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user