feat(security): Phase 1 — critical security fixes and hardening
- #42: Remove hardcoded ENCRYPTION_KEY fallback from config.js, add startup validation for BOT_TOKEN and ENCRYPTION_KEY length - #43: Fix SQL injection vulnerabilities — add ALLOWED_TABLES whitelist in database.js, ALLOWED_USER_FIELDS in userService.js, validate table names before PRAGMA - #44: Fix race condition in purchaseService.js — wrap createPurchase in BEGIN IMMEDIATE TRANSACTION, add atomic balance/stock checks - #41: Move all secrets from docker-compose.yml to .env file, use env_file directive - #45: Replace MD5 tx_hash with crypto.randomUUID() - #46: Upgrade KDF from SHA-256 to HKDF for mnemonic encryption, add backward compatibility for legacy format - #47: Add input validation across all handlers — walletType whitelist, string length limits, numeric ID checks, price bounds New files: - src/utils/encryption.js (HKDF key derivation) - src/__tests__/security.test.js (SQL injection prevention tests) Closes: #41, #42, #43, #44, #45, #46, #47
This commit is contained in:
@@ -39,6 +39,7 @@ export default class AdminDumpHandler {
|
||||
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
|
||||
// Table names must match ALLOWED_TABLES whitelist in database.js
|
||||
const tables = [
|
||||
"categories",
|
||||
"crypto_wallets",
|
||||
@@ -98,6 +99,7 @@ export default class AdminDumpHandler {
|
||||
}
|
||||
|
||||
static async getDumpStatistic() {
|
||||
// Table names must match ALLOWED_TABLES whitelist in database.js
|
||||
const tables = [
|
||||
"categories",
|
||||
"crypto_wallets",
|
||||
|
||||
@@ -6,6 +6,7 @@ import bot from "../../context/bot.js";
|
||||
import CategoryService from "../../services/categoryService.js";
|
||||
import userStates from "../../context/userStates.js";
|
||||
import ProductService from "../../services/productService.js";
|
||||
import Validators from '../../utils/validators.js';
|
||||
|
||||
export default class AdminProductHandler {
|
||||
static isAdmin(userId) {
|
||||
@@ -189,6 +190,11 @@ export default class AdminProductHandler {
|
||||
try {
|
||||
const locationId = state.action.replace('add_category_', '');
|
||||
|
||||
if (!Validators.isValidString(msg.text, 255)) {
|
||||
await bot.sendMessage(chatId, 'Ошибка: недопустимое название категории');
|
||||
return true;
|
||||
}
|
||||
|
||||
await db.runAsync(
|
||||
'INSERT INTO categories (location_id, name) VALUES (?, ?)',
|
||||
[locationId, msg.text]
|
||||
@@ -269,6 +275,11 @@ export default class AdminProductHandler {
|
||||
|
||||
console.log('[DEBUG] Updating category:', { locationId, categoryId, newName: msg.text });
|
||||
|
||||
if (!Validators.isValidString(msg.text, 255)) {
|
||||
await bot.sendMessage(chatId, 'Ошибка: недопустимое название категории');
|
||||
return true;
|
||||
}
|
||||
|
||||
await db.runAsync(
|
||||
'UPDATE categories SET name = ? WHERE id = ? AND location_id = ?',
|
||||
[msg.text, categoryId, locationId]
|
||||
@@ -380,6 +391,11 @@ export default class AdminProductHandler {
|
||||
try {
|
||||
const [locationId, categoryId] = state.action.replace('add_subcategory_', '').split('_');
|
||||
|
||||
if (!Validators.isValidString(msg.text, 255)) {
|
||||
await bot.sendMessage(chatId, 'Ошибка: недопустимое название подкатегории');
|
||||
return true;
|
||||
}
|
||||
|
||||
await db.runAsync(
|
||||
'INSERT INTO subcategories (category_id, name) VALUES (?, ?)',
|
||||
[categoryId, msg.text]
|
||||
@@ -680,6 +696,21 @@ export default class AdminProductHandler {
|
||||
await db.runAsync('BEGIN TRANSACTION');
|
||||
|
||||
for (const product of products) {
|
||||
if (!Validators.isValidString(product.name, 255)) {
|
||||
await bot.sendMessage(chatId, `Ошибка: недопустимое название товара "${product.name}"`);
|
||||
await db.runAsync('ROLLBACK');
|
||||
return true;
|
||||
}
|
||||
if (!Validators.isValidPrice(product.price)) {
|
||||
await bot.sendMessage(chatId, `Ошибка: недопустимая цена "${product.price}"`);
|
||||
await db.runAsync('ROLLBACK');
|
||||
return true;
|
||||
}
|
||||
if (!Number.isFinite(product.quantity_in_stock) || product.quantity_in_stock < 0) {
|
||||
await bot.sendMessage(chatId, `Ошибка: недопустимое количество "${product.quantity_in_stock}"`);
|
||||
await db.runAsync('ROLLBACK');
|
||||
return true;
|
||||
}
|
||||
await db.runAsync(
|
||||
`INSERT INTO products (
|
||||
location_id, category_id,
|
||||
@@ -769,6 +800,17 @@ export default class AdminProductHandler {
|
||||
|
||||
await db.runAsync('BEGIN TRANSACTION');
|
||||
|
||||
if (!Validators.isValidString(product.name, 255)) {
|
||||
await bot.sendMessage(chatId, 'Ошибка: недопустимое название товара');
|
||||
await db.runAsync('ROLLBACK');
|
||||
return true;
|
||||
}
|
||||
if (!Validators.isValidPrice(product.price)) {
|
||||
await bot.sendMessage(chatId, 'Ошибка: недопустимая цена');
|
||||
await db.runAsync('ROLLBACK');
|
||||
return true;
|
||||
}
|
||||
|
||||
await db.runAsync(
|
||||
`UPDATE products SET
|
||||
location_id = ?,
|
||||
|
||||
@@ -7,6 +7,7 @@ import UserService from "../../services/userService.js";
|
||||
import WalletService from "../../services/walletService.js";
|
||||
import PurchaseService from "../../services/purchaseService.js";
|
||||
import userStates from "../../context/userStates.js";
|
||||
import Validators from '../../utils/validators.js';
|
||||
|
||||
export default class AdminUserHandler {
|
||||
static isAdmin(userId) {
|
||||
@@ -471,8 +472,8 @@ export default class AdminUserHandler {
|
||||
|
||||
const newValue = parseFloat(msg.text);
|
||||
|
||||
if (isNaN(newValue)) {
|
||||
await bot.sendMessage(chatId, 'Invalid value. Try again');
|
||||
if (!Validators.isValidBalance(newValue)) {
|
||||
await bot.sendMessage(chatId, 'Invalid value. Must be a non-negative number. Try again');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import bot from "../../context/bot.js";
|
||||
import config from '../../config/config.js';
|
||||
import WalletService from '../../services/walletService.js';
|
||||
import WalletUtils from '../../utils/walletUtils.js';
|
||||
import Validators from '../../utils/validators.js';
|
||||
import fs from 'fs';
|
||||
import csvWriter from 'csv-writer';
|
||||
|
||||
@@ -72,6 +73,11 @@ export default class AdminWalletsHandler {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const walletType = action.split('_').pop();
|
||||
|
||||
if (!Validators.isValidWalletType(walletType)) {
|
||||
await bot.sendMessage(chatId, 'Invalid wallet type.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Удаляем предыдущее сообщение перед отправкой нового
|
||||
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
|
||||
@@ -327,6 +333,11 @@ export default class AdminWalletsHandler {
|
||||
const walletType = match[1]; // Тип кошелька (например, BTC)
|
||||
const pageNumber = parseInt(match[2]); // Номер страницы
|
||||
|
||||
if (!Validators.isValidWalletType(walletType)) {
|
||||
await bot.sendMessage(chatId, 'Invalid wallet type.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Удаляем предыдущее сообщение перед отправкой нового
|
||||
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
|
||||
@@ -350,6 +361,11 @@ export default class AdminWalletsHandler {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const walletType = action.split('_').pop();
|
||||
|
||||
if (!Validators.isValidWalletType(walletType)) {
|
||||
await bot.sendMessage(chatId, 'Invalid wallet type.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`[${new Date().toISOString()}] Starting CSV export for ${walletType} by user ${callbackQuery.from.id}`);
|
||||
|
||||
@@ -497,6 +513,11 @@ export default class AdminWalletsHandler {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const walletType = action.split('_').pop();
|
||||
|
||||
if (!Validators.isValidWalletType(walletType)) {
|
||||
await bot.sendMessage(chatId, 'Invalid wallet type.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`[${new Date().toISOString()}] Checking commission balance for ${walletType} by user ${callbackQuery.from.id}`);
|
||||
|
||||
@@ -560,6 +581,11 @@ export default class AdminWalletsHandler {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const walletType = callbackQuery.data.split('_').pop();
|
||||
|
||||
if (!Validators.isValidWalletType(walletType)) {
|
||||
await bot.sendMessage(chatId, 'Invalid wallet type.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Удаляем предыдущее сообщение перед отправкой нового
|
||||
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
|
||||
|
||||
@@ -7,6 +7,7 @@ import ProductService from "../../services/productService.js";
|
||||
import CategoryService from "../../services/categoryService.js";
|
||||
import UserService from "../../services/userService.js";
|
||||
import PurchaseService from "../../services/purchaseService.js";
|
||||
import Validators from '../../utils/validators.js';
|
||||
|
||||
export default class UserProductHandler {
|
||||
static async showProducts(msg) {
|
||||
@@ -637,6 +638,20 @@ export default class UserProductHandler {
|
||||
const [walletType, productId, quantity] = callbackQuery.data.replace('pay_with_', '').split('_');
|
||||
const state = userStates.get(chatId);
|
||||
|
||||
if (!Validators.isValidWalletType(walletType)) {
|
||||
await bot.sendMessage(chatId, 'Invalid wallet type.');
|
||||
return;
|
||||
}
|
||||
if (!Validators.isValidNumericId(Number(productId))) {
|
||||
await bot.sendMessage(chatId, 'Invalid product.');
|
||||
return;
|
||||
}
|
||||
const qty = Number(quantity);
|
||||
if (!Number.isFinite(qty) || qty <= 0) {
|
||||
await bot.sendMessage(chatId, 'Invalid quantity.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await UserService.recalculateUserBalanceByTelegramId(telegramId);
|
||||
const user = await UserService.getUserByTelegramId(telegramId);
|
||||
|
||||
@@ -10,6 +10,7 @@ import ProductService from "../../services/productService.js";
|
||||
import CategoryService from "../../services/categoryService.js";
|
||||
import bot from "../../context/bot.js";
|
||||
import userStates from "../../context/userStates.js";
|
||||
import Validators from '../../utils/validators.js';
|
||||
|
||||
export default class UserPurchaseHandler {
|
||||
static async viewPurchasePage(userId, page) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import WalletUtils from '../../utils/walletUtils.js';
|
||||
import UserService from "../../services/userService.js";
|
||||
import WalletService from "../../services/walletService.js";
|
||||
import bot from "../../context/bot.js";
|
||||
import Validators from '../../utils/validators.js';
|
||||
|
||||
export default class UserWalletsHandler {
|
||||
|
||||
@@ -355,6 +356,11 @@ export default class UserWalletsHandler {
|
||||
const telegramId = callbackQuery.from.id;
|
||||
const walletType = callbackQuery.data.replace('generate_wallet_', '').replace('_', ' ');
|
||||
|
||||
if (!Validators.isValidWalletType(walletType)) {
|
||||
await bot.sendMessage(chatId, 'Invalid wallet type.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await UserService.getUserByTelegramId(telegramId);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user