Загрузка данных


#!/usr/bin/env python3
"""
HTTP Methods Security Tester (WSTG-CONF-06)
Проверяет поддерживаемые HTTP-методы для заданного веб-приложения.
Автоматически логинится как обычный пользователь и как администратор (если доступны учётные данные).

Использование:
    python test_http_methods.py [--url BASE_URL] [--login LOGIN] [--password PASSWORD] [--admin-login ADMIN_LOGIN] [--admin-password ADMIN_PASSWORD]
"""

import requests
import sys
import argparse
from urllib.parse import urljoin
from requests.exceptions import RequestException, Timeout, ConnectionError
import json
from typing import Dict, List, Tuple, Optional

# ---------- Конфигурация ----------
DEFAULT_URL = "http://0.0.0.1:5000"
# Список эндпоинтов для тестирования (можно расширить или получить динамически через /routes)
# Важно: учитываем CSRF-токены не нужны для простой проверки методов.
ENDPOINTS = [
    "/",
    "/register",
    "/login",
    "/home",
    "/home_admin",
    "/add_card",
    "/recharge_balance",
    "/delete_card",
    "/cart",
    "/checkout",
    "/like",
    "/users",
    "/logout",
    "/news",
    "/admin/news",
    "/admin/news/add",
    "/admin/news/edit/1",     # пример с ID
    "/admin/news/delete/1",   # DELETE обычно, но у нас POST
    "/edit_user/1",
    "/delete_game/1",         # GET на удаление
    "/search_suggestions",
    "/search_suggestions1",
    "/add_game",
    "/editor_game/1",         # GET/POST
    "/ban_user/1",            # POST
    "/add_to_cart/1",
    "/remove_from_cart/1",
    "/add_to_likes/1",
    "/remove_from_likes/1",
]

# Методы для тестирования (включая потенциально опасные)
TEST_METHODS = [
    "GET",
    "POST",
    "PUT",
    "DELETE",
    "PATCH",
    "OPTIONS",
    "HEAD",
    "TRACE",
    "CONNECT",
    "FOO"           # нестандартный метод
]

# Коды ответов, которые считаются признаком того, что метод поддерживается
ALLOWED_STATUS_CODES = {200, 201, 202, 204, 301, 302, 303, 304, 307, 308}
# Опасные методы, на которые стоит обратить особое внимание
DANGEROUS_METHODS = {"PUT", "DELETE", "TRACE", "CONNECT", "PATCH"}

# ---------- Вспомогательные функции ----------
def setup_session(base_url: str, login: Optional[str], password: Optional[str]) -> requests.Session:
    """Создаёт сессию requests и выполняет логин, если указаны учётные данные."""
    session = requests.Session()
    if login and password:
        # Получаем CSRF-токен (если нужен) – в данном приложении Flask-WTF не используется,
        # но для регистрации/логина требуется только email/password.
        login_url = urljoin(base_url, "/login")
        try:
            # Сначала GET запрос на /login, чтобы получить форму (необязательно, но может понадобиться для cookie)
            session.get(login_url, timeout=5)
            # Отправляем POST с данными
            payload = {"email": login, "password": password, "submit": "Sign In"}
            resp = session.post(login_url, data=payload, timeout=5)
            if resp.status_code == 200 and "home" in resp.url:
                print(f"[+] Успешный вход как {login}")
                return session
            else:
                print(f"[-] Не удалось войти как {login} (статус {resp.status_code})")
        except RequestException as e:
            print(f"[-] Ошибка при логине: {e}")
    return session

def test_methods_on_endpoint(session: requests.Session, base_url: str, endpoint: str, methods: List[str]) -> Dict[str, int]:
    """Отправляет запросы с разными методами на указанный endpoint и возвращает словарь {метод: статус-код}."""
    url = urljoin(base_url, endpoint)
    results = {}
    for method in methods:
        try:
            # Для HEAD-запросов не следует читать тело ответа
            if method == "HEAD":
                resp = session.head(url, timeout=5, allow_redirects=False)
            else:
                resp = session.request(method, url, timeout=5, allow_redirects=False)
            results[method] = resp.status_code
        except (ConnectionError, Timeout) as e:
            results[method] = f"Ошибка: {str(e)}"
        except RequestException as e:
            results[method] = f"Исключение: {str(e)}"
    return results

def analyze_results(endpoint: str, results: Dict[str, int], authenticated: bool):
    """Анализирует результаты и выводит предупреждения."""
    print(f"\n--- Эндпоинт: {endpoint} (Авторизация: {'Да' if authenticated else 'Нет'}) ---")
    for method, status in results.items():
        # Игнорируем ошибки соединения для краткости
        if isinstance(status, str) and "Ошибка" in status:
            print(f"  {method:<8} -> {status}")
            continue

        is_allowed = status in ALLOWED_STATUS_CODES
        symbol = "[+]" if is_allowed else "[-]"
        extra = ""
        if is_allowed and method in DANGEROUS_METHODS:
            extra = " !!! ОПАСНЫЙ МЕТОД РАЗРЕШЁН !!!"
        elif not is_allowed and status == 405:
            extra = " (ожидаемый запрет)"
        print(f"  {method:<8} -> {status} {symbol}{extra}")

def generate_report(results_by_endpoint: Dict[str, Dict[str, Dict[str, int]]]):
    """Печатает общий отчёт с опасными методами."""
    print("\n" + "="*80)
    print("ИТОГОВЫЙ ОТЧЁТ ПО ОПАСНЫМ МЕТОДАМ")
    print("="*80)
    dangerous_found = False
    for endpoint, auth_levels in results_by_endpoint.items():
        for auth_name, results in auth_levels.items():
            for method, status in results.items():
                if method in DANGEROUS_METHODS and status in ALLOWED_STATUS_CODES:
                    print(f"[!] {endpoint} [{auth_name}]: метод {method} разрешён (код {status})")
                    dangerous_found = True
    if not dangerous_found:
        print("[+] Опасные методы (PUT, DELETE, TRACE, CONNECT, PATCH) нигде не разрешены.")
    print("="*80)

# ---------- Основная функция ----------
def main():
    parser = argparse.ArgumentParser(description="Тестирование HTTP-методов (WSTG-CONF-06)")
    parser.add_argument("--url", default=DEFAULT_URL, help="Базовый URL приложения (по умолчанию {})".format(DEFAULT_URL))
    parser.add_argument("--login", default=None, help="Email обычного пользователя для теста")
    parser.add_argument("--password", default=None, help="Пароль обычного пользователя")
    parser.add_argument("--admin-login", default=None, help="Email администратора")
    parser.add_argument("--admin-password", default=None, help="Пароль администратора")
    args = parser.parse_args()

    base_url = args.url.rstrip("/")
    print(f"[*] Начинаем тестирование HTTP-методов для {base_url}")
    print(f"[*] Эндпоинтов для проверки: {len(ENDPOINTS)}")

    # Результаты: для каждого эндпоинта храним результаты анонимно, от юзера, от админа
    all_results = {}

    # 1. Анонимные запросы (без сессии)
    print("\n[1] ТЕСТИРОВАНИЕ БЕЗ АУТЕНТИФИКАЦИИ")
    anon_session = requests.Session()
    for endpoint in ENDPOINTS:
        results = test_methods_on_endpoint(anon_session, base_url, endpoint, TEST_METHODS)
        all_results.setdefault(endpoint, {})["anon"] = results
        analyze_results(endpoint, results, authenticated=False)

    # 2. Запросы от обычного пользователя (если указаны данные)
    if args.login and args.password:
        print("\n[2] ТЕСТИРОВАНИЕ ОТ ИМЕНИ ОБЫЧНОГО ПОЛЬЗОВАТЕЛЯ")
        user_session = setup_session(base_url, args.login, args.password)
        for endpoint in ENDPOINTS:
            results = test_methods_on_endpoint(user_session, base_url, endpoint, TEST_METHODS)
            all_results.setdefault(endpoint, {})["user"] = results
            analyze_results(endpoint, results, authenticated=True)
    else:
        print("\n[2] Пропуск тестов от обычного пользователя (укажите --login и --password)")

    # 3. Запросы от администратора
    if args.admin_login and args.admin_password:
        print("\n[3] ТЕСТИРОВАНИЕ ОТ ИМЕНИ АДМИНИСТРАТОРА")
        admin_session = setup_session(base_url, args.admin_login, args.admin_password)
        for endpoint in ENDPOINTS:
            results = test_methods_on_endpoint(admin_session, base_url, endpoint, TEST_METHODS)
            all_results.setdefault(endpoint, {})["admin"] = results
            analyze_results(endpoint, results, authenticated=True)
    else:
        print("\n[3] Пропуск тестов от администратора (укажите --admin-login и --admin-password)")

    # Итоговый отчёт
    generate_report(all_results)

if __name__ == "__main__":
    main()