feat(Validators): add server ID validation function to prevent path traversal and unsafe characters; update routes to utilize new validation logic
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m55s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m55s
This commit is contained in:
@@ -195,6 +195,22 @@ function sanitizeString(str) {
|
|||||||
return cleaned.trim();
|
return cleaned.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Валидация идентификатора сервера для использования в S3-ключах и путях.
|
||||||
|
* Запрещает path traversal (.., /, \) и небезопасные символы.
|
||||||
|
* @param {string} serverId - идентификатор сервера
|
||||||
|
* @param {object} opts - { maxLength: number } (по умолчанию 128)
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isValidServerId(serverId, opts = {}) {
|
||||||
|
if (!serverId || typeof serverId !== 'string') return false;
|
||||||
|
const maxLen = opts.maxLength != null ? opts.maxLength : 128;
|
||||||
|
const trimmed = serverId.trim();
|
||||||
|
if (trimmed.length === 0 || trimmed.length > maxLen) return false;
|
||||||
|
if (trimmed.includes('..') || trimmed.includes('/') || trimmed.includes('\\')) return false;
|
||||||
|
return /^[a-zA-Z0-9_.-]+$/.test(trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Проверка на SQL injection паттерны
|
* Проверка на SQL injection паттерны
|
||||||
* @param {string} str - строка для проверки
|
* @param {string} str - строка для проверки
|
||||||
@@ -328,6 +344,7 @@ module.exports = {
|
|||||||
isValidASN,
|
isValidASN,
|
||||||
isValidCommunity,
|
isValidCommunity,
|
||||||
isValidGateway,
|
isValidGateway,
|
||||||
|
isValidServerId,
|
||||||
sanitizeString,
|
sanitizeString,
|
||||||
isSafeSQLString,
|
isSafeSQLString,
|
||||||
isSafeXSSString,
|
isSafeXSSString,
|
||||||
|
|||||||
@@ -298,7 +298,7 @@ async function runBackupNow(req, res) {
|
|||||||
comment: 'Manual backup from UI (/api/mikrotik/backups/run)',
|
comment: 'Manual backup from UI (/api/mikrotik/backups/run)',
|
||||||
});
|
});
|
||||||
|
|
||||||
return sendOk(res, {
|
return res.json({
|
||||||
ok: true,
|
ok: true,
|
||||||
key,
|
key,
|
||||||
serverId,
|
serverId,
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ async function testMikrotikConnection(req, res) {
|
|||||||
const client = createRosClient(creds);
|
const client = createRosClient(creds);
|
||||||
await client.print('system/resource');
|
await client.print('system/resource');
|
||||||
|
|
||||||
return sendOk(res, { ok: true, message: 'Connection successful' });
|
return res.json({ ok: true, message: 'Connection successful' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const msg = error.response?.data?.message || error.message || 'Connection failed';
|
const msg = error.response?.data?.message || error.message || 'Connection failed';
|
||||||
const status = error.response?.status;
|
const status = error.response?.status;
|
||||||
@@ -1302,12 +1302,18 @@ async function speedTestViaTunnel(req, res) {
|
|||||||
* POST /api/mikrotik/run-script
|
* POST /api/mikrotik/run-script
|
||||||
* Body: { serverId, script?: string } — по умолчанию script=update_bgp_filter
|
* Body: { serverId, script?: string } — по умолчанию script=update_bgp_filter
|
||||||
*/
|
*/
|
||||||
|
const SCRIPT_NAME_REGEX = /^[a-zA-Z0-9_-]{1,64}$/;
|
||||||
|
|
||||||
async function runScript(req, res) {
|
async function runScript(req, res) {
|
||||||
try {
|
try {
|
||||||
const { serverId, script = 'update_bgp_filter' } = req.body || {};
|
const { serverId, script = 'update_bgp_filter' } = req.body || {};
|
||||||
if (!serverId) {
|
if (!serverId) {
|
||||||
return sendError(res, 400, 'serverId is required', 'E_BAD_REQUEST');
|
return sendError(res, 400, 'serverId is required', 'E_BAD_REQUEST');
|
||||||
}
|
}
|
||||||
|
const scriptStr = String(script).trim();
|
||||||
|
if (!SCRIPT_NAME_REGEX.test(scriptStr)) {
|
||||||
|
return sendError(res, 400, 'script: only alphanumeric, underscore, hyphen, max 64 chars', 'E_BAD_REQUEST');
|
||||||
|
}
|
||||||
|
|
||||||
const servers = await readServersFromS3();
|
const servers = await readServersFromS3();
|
||||||
const server = servers.find(s => (s.id || s.dns || s.ip) === serverId);
|
const server = servers.find(s => (s.id || s.dns || s.ip) === serverId);
|
||||||
@@ -1324,11 +1330,11 @@ async function runScript(req, res) {
|
|||||||
// Сначала находим скрипт по имени, затем запускаем по .id
|
// Сначала находим скрипт по имени, затем запускаем по .id
|
||||||
const scripts = await client.print('system/script');
|
const scripts = await client.print('system/script');
|
||||||
const list = Array.isArray(scripts?.data) ? scripts.data : (scripts?.data ? [scripts.data] : []);
|
const list = Array.isArray(scripts?.data) ? scripts.data : (scripts?.data ? [scripts.data] : []);
|
||||||
const found = list.find(s => (s.name || s['.id']) === script);
|
const found = list.find(s => (s.name || s['.id']) === scriptStr);
|
||||||
const scriptId = found ? (found['.id'] || found.name) : script;
|
const scriptId = found ? (found['.id'] || found.name) : scriptStr;
|
||||||
await client.command('system/script/run', { '.id': scriptId });
|
await client.command('system/script/run', { '.id': scriptId });
|
||||||
|
|
||||||
return sendOk(res, { ok: true, message: `Скрипт ${script} запущен` });
|
return res.json({ ok: true, message: `Скрипт ${scriptStr} запущен` });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const msg = error.response?.data?.message || error.message || 'Ошибка запуска скрипта';
|
const msg = error.response?.data?.message || error.message || 'Ошибка запуска скрипта';
|
||||||
const status = error.response?.status;
|
const status = error.response?.status;
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ async function runNetworkMapSchedulerNow(req, res) {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
});
|
});
|
||||||
|
|
||||||
return sendOk(res, { message: 'Запуск выполняется в фоне' });
|
return res.json({ ok: true, message: 'Запуск выполняется в фоне' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[scheduler] run-now', e);
|
console.error('[scheduler] run-now', e);
|
||||||
const msg = e?.message || String(e);
|
const msg = e?.message || String(e);
|
||||||
@@ -177,7 +177,7 @@ async function runPingServicesSchedulerNow(req, res) {
|
|||||||
refreshPingServicesCache().catch((err) => {
|
refreshPingServicesCache().catch((err) => {
|
||||||
console.error('[scheduler] ping-services run-now failed', err);
|
console.error('[scheduler] ping-services run-now failed', err);
|
||||||
});
|
});
|
||||||
return sendOk(res, { message: 'Обновление кеша пинг-сервисов запущено в фоне' });
|
return res.json({ ok: true, message: 'Обновление кеша пинг-сервисов запущено в фоне' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[scheduler] ping-services run-now', e);
|
console.error('[scheduler] ping-services run-now', e);
|
||||||
return sendError(res, 500, 'Не удалось запустить', 'E_SCHEDULER');
|
return sendError(res, 500, 'Не удалось запустить', 'E_SCHEDULER');
|
||||||
@@ -237,7 +237,7 @@ async function runUptimeMonitorSchedulerNow(req, res) {
|
|||||||
body: JSON.stringify({ serverId }),
|
body: JSON.stringify({ serverId }),
|
||||||
}).catch((err) => console.warn('[scheduler] uptime check', serverId, err?.message));
|
}).catch((err) => console.warn('[scheduler] uptime check', serverId, err?.message));
|
||||||
}
|
}
|
||||||
return sendOk(res, { message: 'Проверки Uptime Monitor запущены в фоне' });
|
return res.json({ ok: true, message: 'Проверки Uptime Monitor запущены в фоне' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[scheduler] uptime-monitor run-now', e);
|
console.error('[scheduler] uptime-monitor run-now', e);
|
||||||
return sendError(res, 500, 'Не удалось запустить', 'E_SCHEDULER');
|
return sendError(res, 500, 'Не удалось запустить', 'E_SCHEDULER');
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const { sendError, sendOk, checkIfNoneMatch } = require('../middleware/errorHand
|
|||||||
const { GetObjectCommand, HeadObjectCommand } = require('@aws-sdk/client-s3');
|
const { GetObjectCommand, HeadObjectCommand } = require('@aws-sdk/client-s3');
|
||||||
const { streamToString } = require('../services/s3Service');
|
const { streamToString } = require('../services/s3Service');
|
||||||
const { getS3TextWithHeaders, getS3JsonWithHeaders } = require('../utils/s3Helpers');
|
const { getS3TextWithHeaders, getS3JsonWithHeaders } = require('../utils/s3Helpers');
|
||||||
|
const { isValidServerId } = require('../lib/validators');
|
||||||
|
|
||||||
const S3_KEY = 'servers.json';
|
const S3_KEY = 'servers.json';
|
||||||
|
|
||||||
@@ -143,9 +144,19 @@ const serverConfigsListRoutes = {
|
|||||||
post: postServerConfigsList
|
post: postServerConfigsList
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function validateServerIdParam(req, res) {
|
||||||
|
const serverId = req.params?.serverId;
|
||||||
|
if (!serverId || !isValidServerId(serverId)) {
|
||||||
|
sendError(res, 400, 'Invalid serverId: only alphanumeric, dot, dash, underscore allowed', 'E_BAD_REQUEST');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return serverId;
|
||||||
|
}
|
||||||
|
|
||||||
// GET /api/server-configs/:serverId (конкретная конфигурация)
|
// GET /api/server-configs/:serverId (конкретная конфигурация)
|
||||||
async function getServerConfig(req, res) {
|
async function getServerConfig(req, res) {
|
||||||
const { serverId } = req.params;
|
const serverId = validateServerIdParam(req, res);
|
||||||
|
if (serverId === null) return;
|
||||||
await getS3TextWithHeaders(
|
await getS3TextWithHeaders(
|
||||||
`filter-manager/config-${serverId}.txt`,
|
`filter-manager/config-${serverId}.txt`,
|
||||||
req,
|
req,
|
||||||
@@ -159,7 +170,8 @@ async function getServerConfig(req, res) {
|
|||||||
|
|
||||||
// POST /api/server-configs/:serverId (сохранить конфигурацию)
|
// POST /api/server-configs/:serverId (сохранить конфигурацию)
|
||||||
async function postServerConfig(req, res) {
|
async function postServerConfig(req, res) {
|
||||||
const { serverId } = req.params;
|
const serverId = validateServerIdParam(req, res);
|
||||||
|
if (serverId === null) return;
|
||||||
const { config } = req.body;
|
const { config } = req.body;
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
@@ -177,7 +189,8 @@ async function postServerConfig(req, res) {
|
|||||||
|
|
||||||
// DELETE /api/server-configs/:serverId (удалить только конфигурацию)
|
// DELETE /api/server-configs/:serverId (удалить только конфигурацию)
|
||||||
async function deleteServerConfig(req, res) {
|
async function deleteServerConfig(req, res) {
|
||||||
const { serverId } = req.params;
|
const serverId = validateServerIdParam(req, res);
|
||||||
|
if (serverId === null) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await deleteS3Object(`filter-manager/config-${serverId}.txt`);
|
await deleteS3Object(`filter-manager/config-${serverId}.txt`);
|
||||||
@@ -191,7 +204,8 @@ async function deleteServerConfig(req, res) {
|
|||||||
|
|
||||||
// DELETE /api/server-configs/:serverId/complete (удалить конфигурацию и фильтры)
|
// DELETE /api/server-configs/:serverId/complete (удалить конфигурацию и фильтры)
|
||||||
async function deleteServerComplete(req, res) {
|
async function deleteServerComplete(req, res) {
|
||||||
const { serverId } = req.params;
|
const serverId = validateServerIdParam(req, res);
|
||||||
|
if (serverId === null) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.allSettled([
|
await Promise.allSettled([
|
||||||
@@ -208,7 +222,8 @@ async function deleteServerComplete(req, res) {
|
|||||||
|
|
||||||
// GET /api/server-filters/:serverId
|
// GET /api/server-filters/:serverId
|
||||||
async function getServerFilters(req, res) {
|
async function getServerFilters(req, res) {
|
||||||
const { serverId } = req.params;
|
const serverId = validateServerIdParam(req, res);
|
||||||
|
if (serverId === null) return;
|
||||||
await getS3JsonWithHeaders(
|
await getS3JsonWithHeaders(
|
||||||
`filter-manager/server-filters-${serverId}.json`,
|
`filter-manager/server-filters-${serverId}.json`,
|
||||||
req,
|
req,
|
||||||
@@ -219,7 +234,8 @@ async function getServerFilters(req, res) {
|
|||||||
|
|
||||||
// POST /api/server-filters/:serverId
|
// POST /api/server-filters/:serverId
|
||||||
async function postServerFilters(req, res) {
|
async function postServerFilters(req, res) {
|
||||||
const { serverId } = req.params;
|
const serverId = validateServerIdParam(req, res);
|
||||||
|
if (serverId === null) return;
|
||||||
const { filters } = req.body;
|
const { filters } = req.body;
|
||||||
|
|
||||||
if (!Array.isArray(filters)) {
|
if (!Array.isArray(filters)) {
|
||||||
|
|||||||
@@ -120,9 +120,7 @@ function decrypt(encryptedText) {
|
|||||||
}
|
}
|
||||||
console.error('Decryption error details:', {
|
console.error('Decryption error details:', {
|
||||||
error: error.message,
|
error: error.message,
|
||||||
stack: error.stack,
|
encryptedTextLength: encryptedText.length
|
||||||
encryptedTextLength: encryptedText.length,
|
|
||||||
encryptedTextPreview: encryptedText.substring(0, 100)
|
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user