diff --git a/backend/lib/validators.js b/backend/lib/validators.js index 47cc0c0..cebfd84 100644 --- a/backend/lib/validators.js +++ b/backend/lib/validators.js @@ -195,6 +195,22 @@ function sanitizeString(str) { 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 паттерны * @param {string} str - строка для проверки @@ -328,6 +344,7 @@ module.exports = { isValidASN, isValidCommunity, isValidGateway, + isValidServerId, sanitizeString, isSafeSQLString, isSafeXSSString, diff --git a/backend/routes/mikrotikBackupRoutes.js b/backend/routes/mikrotikBackupRoutes.js index 9e516c8..e4bd390 100644 --- a/backend/routes/mikrotikBackupRoutes.js +++ b/backend/routes/mikrotikBackupRoutes.js @@ -298,7 +298,7 @@ async function runBackupNow(req, res) { comment: 'Manual backup from UI (/api/mikrotik/backups/run)', }); - return sendOk(res, { + return res.json({ ok: true, key, serverId, diff --git a/backend/routes/mikrotikConfigRoutes.js b/backend/routes/mikrotikConfigRoutes.js index d7f1bb0..12f8104 100644 --- a/backend/routes/mikrotikConfigRoutes.js +++ b/backend/routes/mikrotikConfigRoutes.js @@ -236,7 +236,7 @@ async function testMikrotikConnection(req, res) { const client = createRosClient(creds); await client.print('system/resource'); - return sendOk(res, { ok: true, message: 'Connection successful' }); + return res.json({ ok: true, message: 'Connection successful' }); } catch (error) { const msg = error.response?.data?.message || error.message || 'Connection failed'; const status = error.response?.status; @@ -1302,12 +1302,18 @@ async function speedTestViaTunnel(req, res) { * POST /api/mikrotik/run-script * Body: { serverId, script?: string } — по умолчанию script=update_bgp_filter */ +const SCRIPT_NAME_REGEX = /^[a-zA-Z0-9_-]{1,64}$/; + async function runScript(req, res) { try { const { serverId, script = 'update_bgp_filter' } = req.body || {}; if (!serverId) { 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 server = servers.find(s => (s.id || s.dns || s.ip) === serverId); @@ -1324,11 +1330,11 @@ async function runScript(req, res) { // Сначала находим скрипт по имени, затем запускаем по .id const scripts = await client.print('system/script'); const list = Array.isArray(scripts?.data) ? scripts.data : (scripts?.data ? [scripts.data] : []); - const found = list.find(s => (s.name || s['.id']) === script); - const scriptId = found ? (found['.id'] || found.name) : script; + const found = list.find(s => (s.name || s['.id']) === scriptStr); + const scriptId = found ? (found['.id'] || found.name) : scriptStr; await client.command('system/script/run', { '.id': scriptId }); - return sendOk(res, { ok: true, message: `Скрипт ${script} запущен` }); + return res.json({ ok: true, message: `Скрипт ${scriptStr} запущен` }); } catch (error) { const msg = error.response?.data?.message || error.message || 'Ошибка запуска скрипта'; const status = error.response?.status; diff --git a/backend/routes/schedulerRoutes.js b/backend/routes/schedulerRoutes.js index 9aa95e1..ff983d1 100644 --- a/backend/routes/schedulerRoutes.js +++ b/backend/routes/schedulerRoutes.js @@ -106,7 +106,7 @@ async function runNetworkMapSchedulerNow(req, res) { } catch (_) {} }); - return sendOk(res, { message: 'Запуск выполняется в фоне' }); + return res.json({ ok: true, message: 'Запуск выполняется в фоне' }); } catch (e) { console.error('[scheduler] run-now', e); const msg = e?.message || String(e); @@ -177,7 +177,7 @@ async function runPingServicesSchedulerNow(req, res) { refreshPingServicesCache().catch((err) => { console.error('[scheduler] ping-services run-now failed', err); }); - return sendOk(res, { message: 'Обновление кеша пинг-сервисов запущено в фоне' }); + return res.json({ ok: true, message: 'Обновление кеша пинг-сервисов запущено в фоне' }); } catch (e) { console.error('[scheduler] ping-services run-now', e); return sendError(res, 500, 'Не удалось запустить', 'E_SCHEDULER'); @@ -237,7 +237,7 @@ async function runUptimeMonitorSchedulerNow(req, res) { body: JSON.stringify({ serverId }), }).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) { console.error('[scheduler] uptime-monitor run-now', e); return sendError(res, 500, 'Не удалось запустить', 'E_SCHEDULER'); diff --git a/backend/routes/serverConfigsRoutes.js b/backend/routes/serverConfigsRoutes.js index 4a52243..6d8ce36 100644 --- a/backend/routes/serverConfigsRoutes.js +++ b/backend/routes/serverConfigsRoutes.js @@ -10,6 +10,7 @@ const { sendError, sendOk, checkIfNoneMatch } = require('../middleware/errorHand const { GetObjectCommand, HeadObjectCommand } = require('@aws-sdk/client-s3'); const { streamToString } = require('../services/s3Service'); const { getS3TextWithHeaders, getS3JsonWithHeaders } = require('../utils/s3Helpers'); +const { isValidServerId } = require('../lib/validators'); const S3_KEY = 'servers.json'; @@ -143,9 +144,19 @@ const serverConfigsListRoutes = { 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 (конкретная конфигурация) async function getServerConfig(req, res) { - const { serverId } = req.params; + const serverId = validateServerIdParam(req, res); + if (serverId === null) return; await getS3TextWithHeaders( `filter-manager/config-${serverId}.txt`, req, @@ -159,7 +170,8 @@ async function getServerConfig(req, res) { // POST /api/server-configs/:serverId (сохранить конфигурацию) async function postServerConfig(req, res) { - const { serverId } = req.params; + const serverId = validateServerIdParam(req, res); + if (serverId === null) return; const { config } = req.body; if (!config) { @@ -177,7 +189,8 @@ async function postServerConfig(req, res) { // DELETE /api/server-configs/:serverId (удалить только конфигурацию) async function deleteServerConfig(req, res) { - const { serverId } = req.params; + const serverId = validateServerIdParam(req, res); + if (serverId === null) return; try { await deleteS3Object(`filter-manager/config-${serverId}.txt`); @@ -191,7 +204,8 @@ async function deleteServerConfig(req, res) { // DELETE /api/server-configs/:serverId/complete (удалить конфигурацию и фильтры) async function deleteServerComplete(req, res) { - const { serverId } = req.params; + const serverId = validateServerIdParam(req, res); + if (serverId === null) return; try { await Promise.allSettled([ @@ -208,7 +222,8 @@ async function deleteServerComplete(req, res) { // GET /api/server-filters/:serverId async function getServerFilters(req, res) { - const { serverId } = req.params; + const serverId = validateServerIdParam(req, res); + if (serverId === null) return; await getS3JsonWithHeaders( `filter-manager/server-filters-${serverId}.json`, req, @@ -219,7 +234,8 @@ async function getServerFilters(req, res) { // POST /api/server-filters/:serverId async function postServerFilters(req, res) { - const { serverId } = req.params; + const serverId = validateServerIdParam(req, res); + if (serverId === null) return; const { filters } = req.body; if (!Array.isArray(filters)) { diff --git a/backend/utils/encryption.js b/backend/utils/encryption.js index 9e0cf76..b260275 100644 --- a/backend/utils/encryption.js +++ b/backend/utils/encryption.js @@ -120,9 +120,7 @@ function decrypt(encryptedText) { } console.error('Decryption error details:', { error: error.message, - stack: error.stack, - encryptedTextLength: encryptedText.length, - encryptedTextPreview: encryptedText.substring(0, 100) + encryptedTextLength: encryptedText.length }); throw error; }