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

@@ -235,31 +235,19 @@
$spinner.removeClass('is-active');
if (response.success) {
$message
.removeClass('notice-error')
.addClass('notice notice-success')
.html('<p>' + response.data.message + '</p>')
.show();
showNotice($message, response.data.message, 'success');
// Reload page after successful validation/activation.
setTimeout(function() {
location.reload();
}, 1500);
} else {
$message
.removeClass('notice-success')
.addClass('notice notice-error')
.html('<p>' + (response.data.message || 'An error occurred.') + '</p>')
.show();
showNotice($message, response.data.message || 'An error occurred.', 'error');
}
},
error: function() {
$spinner.removeClass('is-active');
$message
.removeClass('notice-success')
.addClass('notice notice-error')
.html('<p>Connection error. Please try again.</p>')
.show();
showNotice($message, 'Connection error. Please try again.', 'error');
}
});
}
@@ -287,30 +275,18 @@
$spinner.removeClass('is-active');
if (response.success) {
$message
.removeClass('notice-error')
.addClass('notice notice-success')
.html('<p>' + response.data.message + '</p>')
.show();
showNotice($message, response.data.message, 'success');
setTimeout(function() {
window.location.href = window.location.pathname + '?page=wp-prometheus&tab=custom';
}, 1000);
} else {
$message
.removeClass('notice-success')
.addClass('notice notice-error')
.html('<p>' + (response.data.message || 'An error occurred.') + '</p>')
.show();
showNotice($message, response.data.message || 'An error occurred.', 'error');
}
},
error: function() {
$spinner.removeClass('is-active');
$message
.removeClass('notice-success')
.addClass('notice notice-error')
.html('<p>Connection error. Please try again.</p>')
.show();
showNotice($message, 'Connection error. Please try again.', 'error');
}
});
}
@@ -398,11 +374,7 @@
$spinner.removeClass('is-active');
if (response.success) {
$message
.removeClass('notice-error')
.addClass('notice notice-success')
.html('<p>' + response.data.message + '</p>')
.show();
showNotice($message, response.data.message, 'success');
$('#import-options').slideUp();
$('#import-metrics-file').val('');
@@ -412,20 +384,12 @@
location.reload();
}, 1500);
} else {
$message
.removeClass('notice-success')
.addClass('notice notice-error')
.html('<p>' + (response.data.message || 'An error occurred.') + '</p>')
.show();
showNotice($message, response.data.message || 'An error occurred.', 'error');
}
},
error: function() {
$spinner.removeClass('is-active');
$message
.removeClass('notice-success')
.addClass('notice notice-error')
.html('<p>Connection error. Please try again.</p>')
.show();
showNotice($message, 'Connection error. Please try again.', 'error');
}
});
}
@@ -478,26 +442,14 @@
$spinner.removeClass('is-active');
if (response.success) {
$message
.removeClass('notice-error')
.addClass('notice notice-success')
.html('<p>' + response.data.message + '</p>')
.show();
showNotice($message, response.data.message, 'success');
} else {
$message
.removeClass('notice-success')
.addClass('notice notice-error')
.html('<p>' + (response.data.message || 'An error occurred.') + '</p>')
.show();
showNotice($message, response.data.message || 'An error occurred.', 'error');
}
},
error: function() {
$spinner.removeClass('is-active');
$message
.removeClass('notice-success')
.addClass('notice notice-error')
.html('<p>Connection error. Please try again.</p>')
.show();
showNotice($message, 'Connection error. Please try again.', 'error');
}
});
}
@@ -546,15 +498,30 @@
// Remove all inputs except the value and button.
$row.find('input').remove();
// Re-add label inputs.
// Re-add label inputs using safe DOM construction.
for (var i = 0; i < labelCount; i++) {
var val = currentValues[i] || '';
$row.prepend('<input type="text" name="label_values[' + rowIndex + '][]" class="small-text" placeholder="' + (labels[i] || 'value') + '" value="' + val + '">');
var $input = $('<input>', {
type: 'text',
name: 'label_values[' + rowIndex + '][]',
'class': 'small-text',
placeholder: labels[i] || 'value',
value: val
});
$row.prepend($input);
}
// Re-add value input.
// Re-add value input using safe DOM construction.
var metricVal = currentValues[currentValues.length - 1] || '';
$row.find('.remove-value-row').before('<input type="number" name="label_values[' + rowIndex + '][]" class="small-text" step="any" placeholder="Value" value="' + metricVal + '">');
var $valueInput = $('<input>', {
type: 'number',
name: 'label_values[' + rowIndex + '][]',
'class': 'small-text',
step: 'any',
placeholder: 'Value',
value: metricVal
});
$row.find('.remove-value-row').before($valueInput);
});
}
@@ -584,7 +551,7 @@
}
/**
* Generate a random token.
* Generate a cryptographically secure random token.
*
* @param {number} length Token length.
* @return {string} Generated token.
@@ -592,12 +559,31 @@
function generateToken(length) {
var charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
var token = '';
var randomValues = new Uint32Array(length);
window.crypto.getRandomValues(randomValues);
for (var i = 0; i < length; i++) {
token += charset.charAt(Math.floor(Math.random() * charset.length));
token += charset.charAt(randomValues[i] % charset.length);
}
return token;
}
/**
* Show a notice message safely (XSS-safe).
*
* @param {jQuery} $element The message container element.
* @param {string} message The message text.
* @param {string} type Notice type: 'success', 'error', or 'warning'.
*/
function showNotice($element, message, type) {
var removeClasses = 'notice-error notice-success notice-warning';
$element
.removeClass(removeClasses)
.addClass('notice notice-' + type)
.empty()
.append($('<p>').text(message))
.show();
}
/**
* Download a file.
*
@@ -666,12 +652,8 @@
$spinner.removeClass('is-active');
if (response.success) {
var noticeClass = response.data.warning ? 'notice-warning' : 'notice-success';
$message
.removeClass('notice-error notice-success notice-warning')
.addClass('notice ' + noticeClass)
.html('<p>' + response.data.message + '</p>')
.show();
var type = response.data.warning ? 'warning' : 'success';
showNotice($message, response.data.message, type);
if (!response.data.warning) {
setTimeout(function() {
@@ -679,20 +661,12 @@
}, 1500);
}
} else {
$message
.removeClass('notice-success notice-warning')
.addClass('notice notice-error')
.html('<p>' + (response.data.message || 'An error occurred.') + '</p>')
.show();
showNotice($message, response.data.message || 'An error occurred.', 'error');
}
},
error: function() {
$spinner.removeClass('is-active');
$message
.removeClass('notice-success notice-warning')
.addClass('notice notice-error')
.html('<p>Connection error. Please try again.</p>')
.show();
showNotice($message, 'Connection error. Please try again.', 'error');
}
});
}
@@ -720,26 +694,14 @@
$spinner.removeClass('is-active');
if (response.success) {
$message
.removeClass('notice-error notice-warning')
.addClass('notice notice-success')
.html('<p>' + response.data.message + '</p>')
.show();
showNotice($message, response.data.message, 'success');
} else {
$message
.removeClass('notice-success notice-warning')
.addClass('notice notice-error')
.html('<p>' + (response.data.message || 'Connection test failed.') + '</p>')
.show();
showNotice($message, response.data.message || 'Connection test failed.', 'error');
}
},
error: function() {
$spinner.removeClass('is-active');
$message
.removeClass('notice-success notice-warning')
.addClass('notice notice-error')
.html('<p>Connection error. Please try again.</p>')
.show();
showNotice($message, 'Connection error. Please try again.', 'error');
}
});
}