54 lines
1.5 KiB
JavaScript
54 lines
1.5 KiB
JavaScript
/**
|
|
* SQL Validator
|
|
*
|
|
* Shared validation for read-only SQL queries (widgets, admin SQL executor).
|
|
* Ensures queries are SELECT-only and don't contain dangerous patterns.
|
|
*/
|
|
|
|
const DEFAULT_MAX_LENGTH = 5000;
|
|
const RESTRICTED_FUNCTIONS = /\b(pg_sleep|set_config|copy)\b/i;
|
|
|
|
/**
|
|
* Validate a SQL query for read-only execution
|
|
* @param {string} sql - The SQL query to validate
|
|
* @param {object} options - Validation options
|
|
* @param {number} [options.maxLength=5000] - Maximum allowed query length
|
|
* @returns {{ valid: boolean, error?: string, normalized?: string }}
|
|
*/
|
|
function validateReadOnlySql(sql, options = {}) {
|
|
const maxLength = options.maxLength || DEFAULT_MAX_LENGTH;
|
|
|
|
if (typeof sql !== 'string' || !sql.trim()) {
|
|
return { valid: false, error: 'SQL query must be a non-empty string' };
|
|
}
|
|
|
|
if (sql.length > maxLength) {
|
|
return {
|
|
valid: false,
|
|
error: `SQL query is too long (max ${maxLength} characters)`,
|
|
};
|
|
}
|
|
|
|
const normalized = sql.trim().replace(/;+\s*$/, '');
|
|
|
|
if (!/^(select|with)\b/i.test(normalized)) {
|
|
return { valid: false, error: 'Only SELECT statements are allowed' };
|
|
}
|
|
|
|
if (normalized.includes(';')) {
|
|
return { valid: false, error: 'Only a single statement is allowed' };
|
|
}
|
|
|
|
if (/--|\/\*/.test(normalized)) {
|
|
return { valid: false, error: 'SQL comments are not allowed' };
|
|
}
|
|
|
|
if (RESTRICTED_FUNCTIONS.test(normalized)) {
|
|
return { valid: false, error: 'Restricted SQL function detected' };
|
|
}
|
|
|
|
return { valid: true, normalized };
|
|
}
|
|
|
|
module.exports = { validateReadOnlySql };
|