Програмне забезпечення керує критичною інфраструктурою — від банківських систем до ядерних об’єктів, отже питання безпеки коду стає надважливим. Кожен рік кіберзлочинці завдають мільярдних збитків, експлуатуючи вразливості, які часто виникають через прості помилки програмування.
- 1. SQL-ін’єкції: коли база даних стає зброєю проти вас
- 2. Переповнення буфера: коли пам’ять виходить з-під контролю
- 3. Cross-Site Scripting (XSS): коли браузер стає ворогом
- 4. Небезпечна десеріалізація: коли дані стають кодом
- 5. Облікові дані зашиті в код: відкриті двері для атакуючих
- 6. Відсутність перевірки та валідації вхідних даних
- 7. Небезпечне використання криптографії
- 8. Проблеми з аутентифікацією та управлінням сесіями
- 9. Race Conditions: коли час має значення
- 10. Недостатнє логування та моніторинг
- Висновки та рекомендації
Ця стаття розглядає найбільш критичні помилки, які роблять розробники, створюючи потенційні “чорні ходи” для хакерів. Розуміння цих помилок — перший крок до написання безпечного коду.
1. SQL-ін’єкції: коли база даних стає зброєю проти вас
SQL-ін’єкція залишається однією з найпоширеніших та найнебезпечніших вразливостей. Вона виникає, коли програма формує SQL-запити, безпосередньо підставляючи в них дані від користувача без належної перевірки.
Як це працює
Уявіть просту форму входу, де програма перевіряє логін та пароль таким чином:
SELECT * FROM users WHERE username = '$username' AND password = '$password'
Якщо зловмисник введе як ім’я користувача рядок admin' --, запит перетвориться на:
SELECT * FROM users WHERE username = 'admin' --' AND password = ''
Символи -- в SQL означають початок коментаря, тому перевірка пароля просто ігнорується. Атакуючий отримує доступ до облікового запису адміністратора без знання пароля.
Наслідки
SQL-ін’єкції можуть призвести до:
- Крадіжки всієї бази даних
- Видалення критичних даних
- Модифікації записів (наприклад, зміни балансів рахунків)
- Отримання повного контролю над сервером бази даних
Як запобігти
Головне правило — ніколи не довіряйте даним від користувача. Використовуйте параметризовані запити або підготовлені вирази (prepared statements), які розділяють SQL-код від даних:
# Правильно
cursor.execute("SELECT * FROM users WHERE username = ? AND password = ?", (username, password))
# Неправильно
cursor.execute(f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'")
2. Переповнення буфера: коли пам’ять виходить з-під контролю
Переповнення буфера — це класична вразливість, особливо характерна для мов програмування низького рівня, таких як C та C++. Вона виникає, коли програма записує дані за межі виділеного буфера пам’яті.
Механізм експлуатації
Коли програма виділяє буфер фіксованого розміру, але не перевіряє розмір даних, що записуються:
char buffer[10];
strcpy(buffer, user_input); // Небезпечно!
Якщо user_input містить більше 10 символів, зайві дані перезапишуть сусідні ділянки пам’яті. Це може включати адреси повернення функцій, що дозволяє атакуючому перенаправити виконання програми на свій код.
Чому це критично
Переповнення буфера може дозволити:
- Виконання довільного коду з правами програми
- Обхід механізмів аутентифікації
- Крах програми (DoS-атака)
- Ескалацію привілеїв до рівня системного адміністратора
Методи захисту
Використовуйте безпечні функції, які враховують розмір буфера:
// Замість strcpy використовуйте strncpy
strncpy(buffer, user_input, sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0';
// Або ще краще - безпечні функції з перевіркою меж
strlcpy(buffer, user_input, sizeof(buffer));
Також важливо використовувати сучасні механізми захисту на рівні компілятора та операційної системи: DEP (Data Execution Prevention), ASLR (Address Space Layout Randomization), та canary values.
3. Cross-Site Scripting (XSS): коли браузер стає ворогом
XSS-атаки дозволяють зловмисникам впроваджувати JavaScript-код на веб-сторінки, які переглядають інші користувачі. Це одна з найпоширеніших вразливостей веб-додатків.
Види XSS
Reflected XSS: зловмисний скрипт передається через URL або форму і одразу виконується:
<!-- Вразливий код -->
<p>Результати пошуку для: <?php echo $_GET['search']; ?></p>
<!-- Атака через URL -->
site.com/search?q=<script>steal_cookies()</script>
Stored XSS: зловмисний код зберігається на сервері (наприклад, у коментарях) і виконується кожного разу, коли хтось переглядає сторінку.
DOM-based XSS: атака відбувається повністю на стороні клієнта через маніпуляції з DOM.
Потенційна шкода
XSS може призвести до:
- Крадіжки сесійних cookies та викрадення облікових записів
- Фішингових атак від імені довіреного сайту
- Встановлення кейлогерів для крадіжки паролів
- Поширення хробаків через соціальні мережі
Захист від XSS
Основний принцип — екранування всіх даних перед виведенням:
// Функція для безпечного виведення тексту
function escapeHtml(text) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return text.replace(/[&<>"']/g, m => map[m]);
}
// Використання
document.getElementById('output').textContent = userInput; // Безпечно
// АБО
document.getElementById('output').innerHTML = escapeHtml(userInput);
Також важливо використовувати Content Security Policy (CSP) заголовки для обмеження виконання скриптів.
4. Небезпечна десеріалізація: коли дані стають кодом
Десеріалізація — це процес перетворення даних (наприклад, JSON, XML, бінарних форматів) назад в об’єкти програми. Небезпечна десеріалізація виникає, коли програма десеріалізує дані з ненадійних джерел без належної перевірки.
Приклад атаки
У багатьох мовах програмування (Java, Python, PHP) десеріалізація може викликати виконання коду:
import pickle
# НІКОЛИ не робіть так з даними від користувача!
user_data = request.get_data()
obj = pickle.loads(user_data) # Небезпечно!
Атакуючий може створити спеціально сформований об’єкт, який при десеріалізації виконає довільний код.
Масштаб проблеми
Небезпечна десеріалізація може дозволити:
- Віддалене виконання коду (RCE)
- Повний контроль над сервером
- Обхід логіки додатку
- DoS-атаки через створення ресурсомістких об’єктів
Рекомендації щодо захисту
Найкращий захист — уникати десеріалізації даних від користувачів. Якщо це неможливо:
# Використовуйте безпечні формати даних
import json
user_data = request.get_json() # JSON безпечніший за pickle
# Валідуйте структуру даних
schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "number"}
}
}
validate(user_data, schema)
5. Облікові дані зашиті в код: відкриті двері для атакуючих
Зберігання паролів, API-ключів та інших секретів прямо в коді — це як залишити ключі під килимком. Це одна з найпростіших для виявлення, але досі дуже поширена помилка.
Чому це проблема
# НІКОЛИ не робіть так!
DATABASE_PASSWORD = "SuperSecret123"
API_KEY = "sk-1234567890abcdef"
def connect_to_database():
return psycopg2.connect(
host="db.example.com",
password=DATABASE_PASSWORD
)
Проблеми:
- Секрети потрапляють у систему контролю версій (Git)
- Будь-хто з доступом до коду отримує доступ до ресурсів
- Неможливо змінити паролі без зміни коду
- Складно використовувати різні облікові дані для різних середовищ
Наслідки витоку
Жорстко закодовані секрети можуть призвести до:
- Несанкціонованого доступу до баз даних
- Використання платних API за ваш рахунок
- Компрометації всієї інфраструктури
- Витоку конфіденційних даних клієнтів
Правильний підхід
Використовуйте змінні середовища або спеціалізовані сервіси управління секретами:
import os
from dotenv import load_dotenv
# Завантаження з файлу .env (не комітіть його!)
load_dotenv()
DATABASE_PASSWORD = os.environ.get('DATABASE_PASSWORD')
API_KEY = os.environ.get('API_KEY')
# Або використовуйте сервіси управління секретами
# AWS Secrets Manager, HashiCorp Vault, Azure Key Vault
6. Відсутність перевірки та валідації вхідних даних
“Ніколи не довіряйте даним від користувача” — це мантра безпечного програмування. Відсутність належної валідації вхідних даних є коренем багатьох вразливостей.
Типові помилки
// Поганий приклад
app.post('/transfer', (req, res) => {
const amount = req.body.amount;
const toAccount = req.body.toAccount;
// Прямо використовуємо дані без перевірки
transferMoney(amount, toAccount);
});
Проблеми:
- Що якщо amount = -1000? (негативний переказ = крадіжка)
- Що якщо amount = “not_a_number”?
- Що якщо toAccount не існує або має неправильний формат?
Комплексний підхід до валідації
// Правильний підхід
app.post('/transfer', (req, res) => {
const amount = parseFloat(req.body.amount);
const toAccount = req.body.toAccount;
// Валідація типу та діапазону
if (isNaN(amount) || amount <= 0 || amount > 10000) {
return res.status(400).json({error: "Invalid amount"});
}
// Валідація формату
if (!/^\d{10}$/.test(toAccount)) {
return res.status(400).json({error: "Invalid account number"});
}
// Додаткова бізнес-логіка
if (amount > getUserBalance(req.user)) {
return res.status(400).json({error: "Insufficient funds"});
}
transferMoney(amount, toAccount);
});
Принципи безпечної валідації
Завжди перевіряйте:
- Тип даних (число, рядок, масив)
- Діапазон значень (мінімум, максимум)
- Формат (регулярні вирази для email, телефонів)
- Довжину рядків
- Наявність обов’язкових полів
- Бізнес-правила (чи має користувач право на цю операцію)
7. Небезпечне використання криптографії
Криптографія — це складно. Навіть досвідчені розробники часто роблять помилки, які повністю нівелюють захист.
Типові помилки
Використання слабких алгоритмів:
import hashlib
# НЕ використовуйте MD5 або SHA1 для паролів!
password_hash = hashlib.md5(password.encode()).hexdigest()
Власні криптографічні рішення:
# НІКОЛИ не придумуйте власну криптографію!
def my_encrypt(text):
return ''.join(chr(ord(c) + 3) for c in text) # Це НЕ шифрування!
Неправильне зберігання паролів:
# Погано - просте хешування
password_hash = hashlib.sha256(password.encode()).hexdigest()
# Добре - з сіллю та повільним алгоритмом
import bcrypt
password_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
Наслідки поганої криптографії
- Паролі можуть бути зламані за лічені хвилини
- “Зашифровані” дані легко розшифровуються
- Підробка цифрових підписів
- Man-in-the-middle атаки
Правильний підхід
# Для паролів використовуйте bcrypt, scrypt або Argon2
import bcrypt
def hash_password(password):
return bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
def verify_password(password, hash):
return bcrypt.checkpw(password.encode(), hash)
# Для шифрування використовуйте перевірені бібліотеки
from cryptography.fernet import Fernet
key = Fernet.generate_key()
cipher = Fernet(key)
encrypted = cipher.encrypt(data.encode())
decrypted = cipher.decrypt(encrypted).decode()
8. Проблеми з аутентифікацією та управлінням сесіями
Навіть якщо ви правильно перевіряєте паролі, помилки в управлінні сесіями можуть звести нанівець всі зусилля з безпеки.
Поширені вразливості
Передбачувані токени сесій:
# Погано - передбачуваний ID
session_id = str(user_id) + str(int(time.time()))
# Добре - криптографічно стійкий випадковий токен
import secrets
session_id = secrets.token_urlsafe(32)
Відсутність таймаутів:
# Сесії повинні мати обмежений термін дії
SESSION_TIMEOUT = 3600 # 1 година
if time.time() - session['created_at'] > SESSION_TIMEOUT:
invalidate_session(session_id)
Незахищена передача:
- Використання HTTP замість HTTPS
- Відсутність флагів Secure та HttpOnly для cookies
- Передача токенів у URL
Комплексний захист сесій
# Правильне налаштування cookies
response.set_cookie(
'session_id',
session_token,
secure=True, # Тільки через HTTPS
httponly=True, # Недоступний для JavaScript
samesite='Strict', # Захист від CSRF
max_age=3600 # Термін дії
)
# Регенерація ID сесії після входу
def login_user(username, password):
if verify_credentials(username, password):
# Створюємо нову сесію для запобігання session fixation
old_session_data = get_session_data()
invalidate_current_session()
new_session_id = create_new_session()
restore_session_data(new_session_id, old_session_data)
9. Race Conditions: коли час має значення
Race conditions виникають, коли результат виконання програми залежить від порядку або часу виконання операцій. У багатопоточних або розподілених системах це може призвести до серйозних проблем безпеки.
Класичний приклад
# Вразливий код для зняття грошей
def withdraw(amount):
balance = get_balance()
if balance >= amount:
# Між перевіркою та оновленням може пройти час!
time.sleep(0.1) # Імітація затримки
new_balance = balance - amount
update_balance(new_balance)
return True
return False
Якщо два запити прийдуть одночасно, обидва можуть пройти перевірку до того, як баланс буде оновлений, дозволяючи зняти більше грошей, ніж є на рахунку.
Експлуатація в реальному світі
Race conditions можуть дозволити:
- Подвійне використання промокодів або купонів
- Перевищення лімітів (наприклад, кількості спроб входу)
- Обхід перевірок безпеки
- Дублювання транзакцій
Методи захисту
import threading
# Використання блокувань
balance_lock = threading.Lock()
def withdraw_safe(amount):
with balance_lock:
balance = get_balance()
if balance >= amount:
new_balance = balance - amount
update_balance(new_balance)
return True
return False
# Або використання транзакцій бази даних
def withdraw_atomic(amount):
with database.transaction():
# База даних гарантує атомарність
cursor.execute("""
UPDATE accounts
SET balance = balance - %s
WHERE id = %s AND balance >= %s
""", (amount, account_id, amount))
return cursor.rowcount > 0
10. Недостатнє логування та моніторинг
Відсутність належного логування — це як керувати автомобілем із зав’язаними очима. Ви не знаєте, що відбувається, поки не станеться катастрофа.
Чому це критично
Без логування ви не можете:
- Виявити атаки в реальному часі
- Розслідувати інциденти
- Зрозуміти масштаб компрометації
- Довести факт атаки для правоохоронних органів
Що потрібно логувати
import logging
from datetime import datetime
# Налаштування логування
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('security.log'),
logging.StreamHandler()
]
)
security_logger = logging.getLogger('security')
# Логування спроб аутентифікації
def login(username, password, ip_address):
if verify_credentials(username, password):
security_logger.info(f"Successful login: user={username}, ip={ip_address}")
return create_session(username)
else:
security_logger.warning(f"Failed login attempt: user={username}, ip={ip_address}")
# Додаткова логіка для виявлення брутфорсу
check_brute_force(username, ip_address)
return None
# Логування критичних операцій
def delete_user_data(user_id, admin_id):
security_logger.critical(
f"Data deletion: target_user={user_id}, admin={admin_id}, "
f"timestamp={datetime.utcnow().isoformat()}"
)
# Виконання операції
Що НЕ логувати
Ніколи не записуйте в логи:
- Паролі (навіть хешовані)
- Номери кредитних карток
- Персональні дані без необхідності
- Токени доступу та API-ключі
Моніторинг та алерти
# Автоматичне виявлення аномалій
def check_suspicious_activity(user_id):
recent_actions = get_user_actions(user_id, last_hour=1)
if len(recent_actions) > 100:
alert_security_team(f"Unusual activity: {len(recent_actions)} actions in 1 hour")
failed_logins = count_failed_logins(user_id, last_minutes=10)
if failed_logins > 5:
temporary_block_user(user_id)
alert_security_team(f"Possible brute force attack on user {user_id}")
Висновки та рекомендації
Безпека програмного забезпечення — це не одноразова дія, а постійний процес. Кожна з розглянутих помилок може стати критичною вразливістю, але знання про них — це перший крок до створення безпечного коду.
Ключові принципи безпечного програмування:
- Принцип найменших привілеїв: надавайте мінімально необхідні права доступу
- Захист в глибину: використовуйте кілька рівнів захисту
- Fail securely: у разі помилки система повинна залишатися в безпечному стані
- Не довіряйте нікому: перевіряйте всі вхідні дані
- Простота: складний код важче захистити
План дій для розробників:
- Проведіть аудит існуючого коду на наявність описаних вразливостей
- Впровадьте автоматизовані інструменти перевірки безпеки (SAST, DAST)
- Організуйте регулярне навчання команди з питань безпеки
- Включіть тестування безпеки в процес розробки (DevSecOps)
- Створіть та дотримуйтесь coding standards з урахуванням безпеки
Пам’ятайте: безпека — це не витрата, а інвестиція. Вартість усунення вразливості зростає експоненційно з часом її існування. Краще запобігти проблемі на етапі написання коду, ніж виправляти наслідки злому.
Безпечний код — це не просто технічна вимога, це відповідальність перед користувачами, які довіряють нам свої дані та своє цифрове життя.


