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

@@ -107,36 +107,8 @@ function wp_prometheus_isolated_metrics_handler(): void {
return; // Let normal flow handle unlicensed state.
}
// Authenticate.
$auth_token = get_option( 'wp_prometheus_auth_token', '' );
if ( empty( $auth_token ) ) {
status_header( 401 );
header( 'WWW-Authenticate: Bearer realm="WP Prometheus Metrics"' );
header( 'Content-Type: text/plain; charset=utf-8' );
echo 'Unauthorized';
exit;
}
// Check Bearer token.
$auth_header = '';
if ( isset( $_SERVER['HTTP_AUTHORIZATION'] ) ) {
$auth_header = sanitize_text_field( wp_unslash( $_SERVER['HTTP_AUTHORIZATION'] ) );
} elseif ( isset( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) ) {
$auth_header = sanitize_text_field( wp_unslash( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) );
}
$authenticated = false;
if ( ! empty( $auth_header ) && preg_match( '/Bearer\s+(.*)$/i', $auth_header, $matches ) ) {
$authenticated = hash_equals( $auth_token, $matches[1] );
}
// Check query parameter fallback.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Auth token check.
if ( ! $authenticated && isset( $_GET['token'] ) ) {
$authenticated = hash_equals( $auth_token, sanitize_text_field( wp_unslash( $_GET['token'] ) ) );
}
if ( ! $authenticated ) {
// Authenticate using shared helper.
if ( ! wp_prometheus_authenticate_request() ) {
status_header( 401 );
header( 'WWW-Authenticate: Bearer realm="WP Prometheus Metrics"' );
header( 'Content-Type: text/plain; charset=utf-8' );
@@ -161,6 +133,64 @@ function wp_prometheus_isolated_metrics_handler(): void {
exit;
}
/**
* Authenticate a metrics request using Bearer token or query parameter.
*
* Shared authentication logic used by both the MetricsEndpoint class
* and the isolated mode handler to avoid code duplication.
*
* @return bool True if authenticated, false otherwise.
*/
function wp_prometheus_authenticate_request(): 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 = wp_prometheus_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.
*
* Checks multiple sources for the Authorization header to support
* different server configurations (Apache, nginx, CGI, etc.).
*
* @return string The Authorization header value, or empty string if not found.
*/
function wp_prometheus_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 '';
}
// Try early metrics handling before full plugin initialization.
wp_prometheus_early_metrics_check();