Безопасность и CSRF защита

Полное руководство по обеспечению безопасности в MNRFY Framework

Основы безопасности

Автоматическое экранирование

MNRFY автоматически экранирует все данные, выводимые через {{ }}, что предотвращает XSS атаки:

<!-- Безопасный вывод - данные автоматически экранируются -->
{{ user.name }}
{{ comment.text }}
{{ request.input('search') }}

<!-- Результат для "<script>alert('XSS')</script>" -->
<!-- Будет показано: &lt;script&gt;alert('XSS')&lt;/script&gt; -->

<!-- Небезопасный сырой вывод - используйте только для доверенных данных! -->
{!! admin_content !!}
{!! page.html_content !!}
Важно! Никогда не используйте {!! !!} для пользовательских данных. Это может привести к XSS атакам.

Валидация входных данных

<!-- Проверка email -->
{% set email = request.input('email') %}
{% if email and filter_var(email, FILTER_VALIDATE_EMAIL) %}
    <p>Email корректный: {{ email }}</p>
{% else %}
    <p class="error">Некорректный email</p>
{% endif %}

<!-- Проверка числовых данных -->
{% set age = request.input('age') %}
{% if is_numeric(age) and age >= 18 and age <= 120 %}
    <p>Возраст: {{ age }} лет</p>
{% else %}
    <p class="error">Некорректный возраст</p>
{% endif %}

<!-- Очистка строк -->
{% set username = request.input('username') | trim | striptags %}
{% if username | length >= 3 and username | length <= 20 %}
    <p>Имя пользователя: {{ username }}</p>
{% else %}
    <p class="error">Имя пользователя должно быть от 3 до 20 символов</p>
{% endif %}

CSRF защита

Основы CSRF защиты

Cross-Site Request Forgery (CSRF) - это атака, когда злоумышленник заставляет пользователя выполнить нежелательные действия на сайте, где он авторизован.

Использование CSRF токенов

<!-- В HTML формах -->
<form method="POST" action="/users/update">
    {{ csrf() }}
    
    <div class="form-group">
        <label>Имя</label>
        <input type="text" name="name" value="{{ user.name }}" required>
    </div>
    
    <button type="submit">Сохранить</button>
</form>

<!-- Альтернативный способ -->
<form method="POST" action="/posts/create">
    <input type="hidden" name="_token" value="{{ csrf_token() }}">
    
    <textarea name="content" required></textarea>
    <button type="submit">Создать пост</button>
</form>

CSRF для AJAX запросов

<!-- Мета-тег с CSRF токеном -->
<meta name="csrf-token" content="{{ csrf_token() }}">

<script>
// Получение CSRF токена
function getCSRFToken() {
    return document.querySelector('meta[name="csrf-token"]').getAttribute('content');
}

// Пример AJAX запроса с CSRF токеном
async function updateProfile(data) {
    try {
        const response = await fetch('/api/profile', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-CSRF-Token': getCSRFToken(),
                'X-Requested-With': 'XMLHttpRequest'
            },
            body: JSON.stringify(data)
        });
        
        const result = await response.json();
        
        if (result.success) {
            showNotification('Профиль обновлен', 'success');
        } else {
            showNotification(result.error || 'Ошибка обновления', 'error');
        }
    } catch (error) {
        showNotification('Ошибка соединения', 'error');
    }
}

// Автоматическое добавление CSRF токена ко всем формам
document.addEventListener('DOMContentLoaded', function() {
    const token = getCSRFToken();
    
    // Добавляем токен ко всем AJAX формам
    document.querySelectorAll('form[data-ajax="true"]').forEach(form => {
        form.addEventListener('submit', function(e) {
            e.preventDefault();
            
            const formData = new FormData(form);
            formData.append('_token', token);
            
            fetch(form.action, {
                method: form.method,
                headers: {
                    'X-CSRF-Token': token,
                    'X-Requested-With': 'XMLHttpRequest'
                },
                body: formData
            })
            .then(response => response.json())
            .then(data => {
                if (data.success) {
                    showNotification(data.message, 'success');
                } else {
                    showNotification(data.error, 'error');
                }
            })
            .catch(error => {
                showNotification('Ошибка запроса', 'error');
            });
        });
    });
    
    // Обновление токена при истечении сессии
    setInterval(() => {
        fetch('/api/csrf-token')
            .then(response => response.json())
            .then(data => {
                if (data.token) {
                    document.querySelector('meta[name="csrf-token"]').setAttribute('content', data.token);
                }
            });
    }, 600000); // Каждые 10 минут
});

function showNotification(message, type) {
    // Реализация уведомлений
    const notification = document.createElement('div');
    notification.className = `notification notification-${type}`;
    notification.textContent = message;
    document.body.appendChild(notification);
    
    setTimeout(() => notification.remove(), 5000);
}
</script>

CSRF в пользовательских обработчиках

<!-- Форма с пользовательским обработчиком -->
<form method="POST" action="/">
    {{ csrf() }}
    <input type="hidden" name="__handler" value="delete-account">
    
    <div class="warning-message">
        <h3>⚠️ Удаление аккаунта</h3>
        <p>Это действие нельзя отменить. Все ваши данные будут удалены навсегда.</p>
    </div>
    
    <div class="form-group">
        <label>Подтвердите удаление, введя "УДАЛИТЬ"</label>
        <input type="text" name="confirmation" required 
               pattern="УДАЛИТЬ" title="Введите точно: УДАЛИТЬ">
    </div>
    
    <div class="form-group">
        <label>Введите ваш пароль</label>
        <input type="password" name="password" required>
    </div>
    
    <button type="submit" class="btn btn-danger" 
            onclick="return confirm('Вы действительно хотите удалить аккаунт?')">
        Удалить аккаунт навсегда
    </button>
</form>

Безопасность баз данных

Предотвращение SQL инъекций

MNRFY автоматически использует подготовленные запросы, что предотвращает SQL инъекции:

<!-- Безопасные запросы через API -->
{% set users = db('1752665380840')->getItems('users', {
    'name': request.input('name'),
    'email': request.input('email')
}) %}

<!-- Безопасный сырой SQL запрос -->
{% set searchResults = db('1752665380840')->query('
    SELECT * FROM users 
    WHERE name LIKE ? AND email = ? AND created_at > ?
', ['%' ~ request.input('search') ~ '%', request.input('email'), strtotime('-30 days')]) %}
Автоматическая защита: Все параметры в запросах MNRFY автоматически экранируются.

Валидация данных перед запросами

<!-- Безопасное обновление пользователя -->
{% if request.isMethod('POST') %}
    {% set userId = request.input('user_id') %}
    {% set newEmail = request.input('email') %}
    {% set newName = request.input('name') %}
    
    {% set errors = [] %}
    
    <!-- Валидация ID пользователя -->
    {% if not is_numeric(userId) or userId <= 0 %}
        {% set errors = errors | merge(['Некорректный ID пользователя']) %}
    {% endif %}
    
    <!-- Валидация email -->
    {% if not filter_var(newEmail, FILTER_VALIDATE_EMAIL) %}
        {% set errors = errors | merge(['Некорректный email']) %}
    {% endif %}
    
    <!-- Валидация имени -->
    {% set cleanName = newName | trim | striptags %}
    {% if cleanName | length < 2 or cleanName | length > 50 %}
        {% set errors = errors | merge(['Имя должно быть от 2 до 50 символов']) %}
    {% endif %}
    
    {% if errors | length == 0 %}
        <!-- Проверяем права доступа -->
        {% if auth() and (auth().id == userId or auth().role == 'admin') %}
            {% set updated = db('1752665380840')->editItem('users', 
                {'id': userId}, 
                {'email': newEmail, 'name': cleanName, 'updated_at': time()}
            ) %}
            
            {% if updated %}
                <div class="alert alert-success">Данные успешно обновлены</div>
            {% else %}
                <div class="alert alert-error">Ошибка обновления данных</div>
            {% endif %}
        {% else %}
            <div class="alert alert-error">Недостаточно прав для выполнения операции</div>
        {% endif %}
    {% else %}
        <div class="alert alert-error">
            {% foreach errors as error %}
                <p>{{ error }}</p>
            {% endforeach %}
        </div>
    {% endif %}
{% endif %}

Контроль доступа

Проверка авторизации

<!-- Базовая проверка авторизации -->
{% if not auth() %}
    <div class="auth-required">
        <h3>🔒 Требуется авторизация</h3>
        <p>Для доступа к этой странице необходимо войти в систему.</p>
        <a href="/login" class="btn btn-primary">Войти</a>
        <a href="/register" class="btn btn-secondary">Регистрация</a>
    </div>
{% else %}
    <!-- Контент для авторизованных пользователей -->
    <div class="user-content">
        <h2>Добро пожаловать, {{ auth().name }}!</h2>
        <!-- Остальной контент -->
    </div>
{% endif %}

Проверка ролей и разрешений

<!-- Проверка роли администратора -->
{% if auth() and auth().role == 'admin' %}
    <div class="admin-panel">
        <h3>👑 Панель администратора</h3>
        <a href="/admin/users" class="btn btn-danger">Управление пользователями</a>
        <a href="/admin/settings" class="btn btn-warning">Настройки системы</a>
    </div>
{% endif %}

<!-- Проверка множественных ролей -->
{% if auth() and auth().role in ['admin', 'moderator'] %}
    <div class="moderation-tools">
        <h3>🛡️ Инструменты модерации</h3>
        <a href="/moderate/comments" class="btn btn-info">Модерировать комментарии</a>
        <a href="/moderate/posts" class="btn btn-info">Модерировать посты</a>
    </div>
{% endif %}

<!-- Проверка прав доступа -->
{% if can('edit_posts') %}
    <a href="/posts/{{ post.id }}/edit" class="btn btn-primary">Редактировать</a>
{% endif %}

{% if can('delete_posts') %}
    <button onclick="deletePost({{ post.id }})" class="btn btn-danger">Удалить</button>
{% endif %}

Проверка владения ресурсом

<!-- Редактирование только своих постов -->
{% if auth() and (post.user_id == auth().id or auth().role == 'admin') %}
    <div class="post-controls">
        <a href="/posts/{{ post.id }}/edit" class="btn btn-primary">Редактировать</a>
        <button onclick="deletePost({{ post.id }})" class="btn btn-danger">Удалить</button>
    </div>
{% endif %}

<!-- Просмотр приватного профиля -->
{% if user.is_private and auth() and user.id != auth().id %}
    <div class="private-profile">
        <h3>🔒 Приватный профиль</h3>
        <p>Этот пользователь скрыл свой профиль от посторонних.</p>
    </div>
{% else %}
    <!-- Показываем профиль -->
    <div class="user-profile">
        <h2>{{ user.name }}</h2>
        <p>{{ user.bio }}</p>
    </div>
{% endif %}

Защита от атак

Защита от перебора (Brute Force)

<!-- Ограничение попыток входа -->
{% set maxAttempts = 5 %}
{% set lockoutTime = 900 %} <!-- 15 минут -->
{% set clientIP = request.ip() %}
{% set attempts = session('login_attempts_' ~ clientIP, 0) %}
{% set lastAttempt = session('last_attempt_' ~ clientIP, 0) %}

{% if attempts >= maxAttempts and (time() - lastAttempt) < lockoutTime %}
    <div class="alert alert-danger">
        <h3>🚫 Аккаунт временно заблокирован</h3>
        <p>Слишком много неудачных попыток входа с вашего IP адреса.</p>
        <p>Попробуйте снова через {{ ((lockoutTime - (time() - lastAttempt)) / 60) | round }} минут.</p>
    </div>
{% else %}
    <form method="POST" action="/login">
        {{ csrf() }}
        
        <div class="form-group">
            <label>Email или логин</label>
            <input type="text" name="login" required autocomplete="username">
        </div>
        
        <div class="form-group">
            <label>Пароль</label>
            <input type="password" name="password" required autocomplete="current-password">
        </div>
        
        {% if attempts > 0 %}
            <div class="alert alert-warning">
                Неудачных попыток: {{ attempts }} из {{ maxAttempts }}
            </div>
        {% endif %}
        
        <button type="submit" class="btn btn-primary">Войти</button>
    </form>
{% endif %}

Защита от ботов (CAPTCHA)

<!-- Простая математическая CAPTCHA -->
{% set num1 = rand(1, 10) %}
{% set num2 = rand(1, 10) %}
{% set captchaAnswer = num1 + num2 %}

<form method="POST" action="/contact">
    {{ csrf() }}
    
    <div class="form-group">
        <label>Имя</label>
        <input type="text" name="name" required>
    </div>
    
    <div class="form-group">
        <label>Email</label>
        <input type="email" name="email" required>
    </div>
    
    <div class="form-group">
        <label>Сообщение</label>
        <textarea name="message" required></textarea>
    </div>
    
    <!-- CAPTCHA -->
    <div class="form-group captcha-group">
        <label>Решите пример: {{ num1 }} + {{ num2 }} = ?</label>
        <input type="number" name="captcha" required min="2" max="20">
        <input type="hidden" name="captcha_answer" value="{{ captchaAnswer | base64_encode }}">
    </div>
    
    <button type="submit" class="btn btn-primary">Отправить</button>
</form>

Защита конфиденциальных данных

Хеширование паролей

<!-- В обработчике регистрации (PHP код) -->
{% php %}
    // Пример безопасного хеширования пароля
    $password = $_POST['password'] ?? '';
    $passwordHash = password_hash($password, PASSWORD_DEFAULT);
    
    // Сохранение в БД
    $userId = db('1752665380840')->addItem('users', [
        'login' => $_POST['login'],
        'email' => $_POST['email'],
        'password_hash' => $passwordHash, // Храним хеш, а не пароль
        'created_at' => time()
    ]);
{% endphp %}

<!-- Проверка пароля при входе -->
{% php %}
    $loginInput = $_POST['login'] ?? '';
    $passwordInput = $_POST['password'] ?? '';
    
    $user = db('1752665380840')->getItem('users', [
        'login' => $loginInput
    ]);
    
    if ($user && password_verify($passwordInput, $user['password_hash'])) {
        // Пароль правильный
        $_SESSION['user_id'] = $user['id'];
        $loginSuccess = true;
    } else {
        // Неправильный пароль
        $loginError = 'Неверный логин или пароль';
    }
{% endphp %}

Защита персональных данных

<!-- Маскирование чувствительных данных -->
<div class="user-info">
    <h3>Информация о пользователе</h3>
    
    <p>Имя: {{ user.name }}</p>
    
    <!-- Маскируем email -->
    {% set emailParts = user.email | split('@') %}
    {% set maskedEmail = emailParts[0] | slice(0, 2) ~ '***@' ~ emailParts[1] %}
    <p>Email: {{ maskedEmail }}</p>
    
    <!-- Маскируем телефон -->
    {% if user.phone %}
        {% set maskedPhone = user.phone | slice(0, 2) ~ '***-**-' ~ user.phone | slice(-2) %}
        <p>Телефон: {{ maskedPhone }}</p>
    {% endif %}
    
    <!-- Показываем полные данные только владельцу -->
    {% if auth() and auth().id == user.id %}
        <div class="full-info">
            <h4>Полная информация</h4>
            <p>Полный email: {{ user.email }}</p>
            {% if user.phone %}
                <p>Полный телефон: {{ user.phone }}</p>
            {% endif %}
        </div>
    {% endif %}
</div>

Комплексная форма с защитой

Безопасная форма регистрации

<form method="POST" action="/" class="secure-registration-form">
    {{ csrf() }}
    <input type="hidden" name="__handler" value="secure-register">
    
    <h2>🔒 Безопасная регистрация</h2>
    
    <div class="form-group">
        <label>Логин *</label>
        <input type="text" name="login" required 
               minlength="3" maxlength="20" 
               pattern="[a-zA-Z0-9_-]+" 
               title="Только буквы, цифры, дефисы и подчеркивания"
               value="{{ old('login') }}">
        <div class="field-requirements">
            3-20 символов, только буквы, цифры, дефисы и подчеркивания
        </div>
    </div>
    
    <div class="form-group">
        <label>Email *</label>
        <input type="email" name="email" required 
               value="{{ old('email') }}">
    </div>
    
    <div class="form-group">
        <label>Пароль *</label>
        <input type="password" name="password" required 
               minlength="8" 
               pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}"
               title="Минимум 8 символов, включая строчные и заглавные буквы, цифры и спецсимволы"
               onkeyup="checkPasswordStrength(this.value)">
        <div id="password-strength" class="password-strength"></div>
        <div class="field-requirements">
            Минимум 8 символов: строчные и заглавные буквы, цифры, спецсимволы
        </div>
    </div>
    
    <div class="form-group">
        <label>Подтверждение пароля *</label>
        <input type="password" name="password_confirmation" required>
    </div>
    
    <!-- Математическая CAPTCHA -->
    {% set captchaNum1 = rand(5, 15) %}
    {% set captchaNum2 = rand(1, 10) %}
    {% set captchaAnswer = captchaNum1 + captchaNum2 %}
    
    <div class="form-group captcha-group">
        <label>Подтвердите, что вы не робот: {{ captchaNum1 }} + {{ captchaNum2 }} = ?</label>
        <input type="number" name="captcha_answer" required min="6" max="25">
        <input type="hidden" name="captcha_correct" value="{{ hash('sha256', captchaAnswer ~ session_id()) }}">
    </div>
    
    <div class="form-group">
        <label class="checkbox-label">
            <input type="checkbox" name="agree_terms" required>
            Я согласен с <a href="/terms" target="_blank">условиями использования</a> 
            и <a href="/privacy" target="_blank">политикой конфиденциальности</a> *
        </label>
    </div>
    
    <div class="form-group">
        <label class="checkbox-label">
            <input type="checkbox" name="newsletter">
            Получать новости и обновления по email
        </label>
    </div>
    
    <!-- Показываем ошибки валидации -->
    {% if flash('errors') %}
        <div class="alert alert-danger">
            <h4>Ошибки валидации:</h4>
            <ul>
            {% foreach flash('errors') as error %}
                <li>{{ error }}</li>
            {% endforeach %}
            </ul>
        </div>
    {% endif %}
    
    <button type="submit" class="btn btn-primary btn-block">
        🔒 Создать аккаунт
    </button>
    
    <p class="form-footer">
        Уже есть аккаунт? <a href="/login">Войти</a>
    </p>
</form>

<script>
function checkPasswordStrength(password) {
    const strengthDiv = document.getElementById('password-strength');
    let score = 0;
    let feedback = [];
    
    if (password.length >= 8) score++;
    else feedback.push('Минимум 8 символов');
    
    if (/[a-z]/.test(password)) score++;
    else feedback.push('Строчные буквы');
    
    if (/[A-Z]/.test(password)) score++;
    else feedback.push('Заглавные буквы');
    
    if (/\d/.test(password)) score++;
    else feedback.push('Цифры');
    
    if (/[@$!%*?&]/.test(password)) score++;
    else feedback.push('Спецсимволы (@$!%*?&)');
    
    const strength = ['Очень слабый', 'Слабый', 'Средний', 'Хороший', 'Отличный'];
    const colors = ['#ff4444', '#ff8800', '#ffaa00', '#88cc00', '#00cc44'];
    
    strengthDiv.innerHTML = `
        <div class="strength-bar" style="background-color: ${colors[score]}; width: ${score * 20}%;"></div>
        <div class="strength-text">Сила пароля: ${strength[score]}</div>
        ${feedback.length > 0 ? `<div class="missing">Не хватает: ${feedback.join(', ')}</div>` : ''}
    `;
}
</script>
Безопасность обеспечена! Вы изучили все аспекты безопасности в MNRFY Framework. Теперь вы можете создавать безопасные веб-приложения.