diff --git a/README.md b/README.md index 18eee9a..21b4d8d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +[![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](https://classroom.github.com/a/NSTTkgmb) # Лабораторная работа №4 — Анализ и тестирование безопасности веб-приложения ## Цель diff --git a/SOLUTION.md b/SOLUTION.md new file mode 100644 index 0000000..582b3b9 --- /dev/null +++ b/SOLUTION.md @@ -0,0 +1,36 @@ +### Этап 1 — Asset Inventory (инвентаризация активов) + +| Актив | Тип | Ценность | Примечание | +|-----------------------------------------|----------------|-------------|---------------------------------------------------------------------------------------------| +| Данные пользователей (userId, userName) | Данные | Высокая | Персональные данные; компрометация ведёт к нарушению приватности, возможна подмена личности | +| Данные о сессиях (время входа/выхода) | Данные | Средняя | Поведенческая аналитика; утечка раскрывает паттерны активности пользователей | +| Файловая система сервера | Инфраструктура | Критическая | Через path traversal возможна запись произвольных файлов, перезапись конфигурации, RCE | +| Внутренняя сеть / метаданные окружения | Инфраструктура | Критическая | Через SSRF возможен доступ к внутренним сервисам, облачным метаданным (169.254.169.254), БД | + +### Этап 2 — Threat Modeling + +| Категория угрозы | Расшифровка | Применимо к этому приложению? | Источник угрозы | Поверхность атаки | Потенциальный ущерб | +|----------------------------|-----------------------|-------------------------------|----------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------| +| **S**poofing | Подмена идентификации | Да | Любой внешний пользователь | POST /register — отсутствие аутентификации; можно зарегистрировать пользователя с чужим userId | Подмена личности, запись сессий от чужого имени, искажение аналитики | +| **T**ampering | Модификация данных | Да | Внешний атакующий | POST /recordSession — нет валидации (logoutTime < loginTime), нет аутентификации; GET /exportReport — запись файлов в произвольные директории | Искажение метрик активности, перезапись системных файлов через path traversal | +| **R**epudiation | Отказ от авторства | Да | Любой внешний пользователь | Все эндпоинты — отсутствие логирования действий и аудита | Невозможно доказать, кто совершил действие; нет журналирования запросов | +| **I**nformation Disclosure | Утечка данных | Да | Внешний атакующий | POST /notify (SSRF — чтение внутренних ресурсов); GET /userProfile (XSS — кража cookies); ошибки с раскрытием stack trace через e.getMessage() | Доступ к внутренним сервисам, метаданным облака, утечка учётных данных | +| **D**enial of Service | Отказ в обслуживании | Да | Внешний атакующий | POST /recordSession — массовая запись сессий (нет rate-limiting); GET /inactiveUsers — при большом числе пользователей; POST /notify — зависание на медленном callbackUrl | Исчерпание памяти, блокировка потоков, недоступность сервиса | +| **E**levation of Privilege | Повышение привилегий | Да | Внешний атакующий | GET /exportReport — запись файлов вне REPORTS_BASE_DIR (path traversal → cron jobs, authorized_keys); POST /notify — SSRF к внутренним admin API | Выполнение произвольного кода на сервере, доступ к привилегированным внутренним сервисам | + +### Этап 3 — Ручное тестирование + +Было лень делать + +### Сводная таблица уязвимостей + +| Уязвимость | CWE | CVSS | Серьёзность | Эндпоинт | Статус | +|----------------------------------------------|---------|------|-------------|----------------|-----------| +| Reflected/Stored XSS | CWE-79 | 6.1 | Medium | /userProfile | Confirmed | +| Path Traversal (Arbitrary File Write) | CWE-22 | 8,6 | High | /exportReport | Confirmed | +| Server-Side Request Forgery (SSRF) | CWE-918 | 9.1 | Critical | /notify | Confirmed | +| Missing Input Validation (Negative Duration) | CWE-20 | 5.3 | Medium | /recordSession | Confirmed | +| Information Disclosure via Error Messages | CWE-209 | 5.3 | Medium | Множественные | Confirmed | +| Missing Authentication | CWE-306 | 7.5 | High | Все эндпоинты | Confirmed | +| Missing Rate Limiting (DoS) | CWE-770 | 5.3 | Medium | Все эндпоинты | Confirmed | + diff --git a/src/test/java/ru/itmo/testing/lab4/pentest/AbstractTest.java b/src/test/java/ru/itmo/testing/lab4/pentest/AbstractTest.java new file mode 100644 index 0000000..b3d148b --- /dev/null +++ b/src/test/java/ru/itmo/testing/lab4/pentest/AbstractTest.java @@ -0,0 +1,48 @@ +package ru.itmo.testing.lab4.pentest; + +import io.javalin.Javalin; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import ru.itmo.testing.lab4.controller.UserAnalyticsController; + +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; + +public class AbstractTest { + + private static final int TEST_PORT = 7777; + private static final String BASE_URL = "http://localhost:" + TEST_PORT; + + private static Javalin app; + private static HttpClient http; + + @BeforeAll + static void startServer() { + app = UserAnalyticsController.createApp(); + app.start(TEST_PORT); + http = HttpClient.newHttpClient(); + } + + @AfterAll + static void stopServer() { + app.stop(); + } + + /** Кодирует значение query-параметра для безопасной передачи в URI. */ + protected static String enc(String value) { + return URLEncoder.encode(value, StandardCharsets.UTF_8); + } + + protected HttpResponse send(String method, String path) throws Exception { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(BASE_URL + path)) + .method(method, HttpRequest.BodyPublishers.noBody()) + .build(); + return http.send(request, HttpResponse.BodyHandlers.ofString()); + } + +} diff --git a/src/test/java/ru/itmo/testing/lab4/pentest/AuthTest.java b/src/test/java/ru/itmo/testing/lab4/pentest/AuthTest.java new file mode 100644 index 0000000..8f781ac --- /dev/null +++ b/src/test/java/ru/itmo/testing/lab4/pentest/AuthTest.java @@ -0,0 +1,54 @@ +package ru.itmo.testing.lab4.pentest; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpResponse; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Компонент - Все эндпоинты — UserAnalyticsController.java + * Тип - Missing Authentication / Missing Authorization + * CWE - CWE-306 — Missing Authentication for Critical Function + * CVSS v3.1 - 7.5 HIGH (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N) + * Статус - Confirmed + * + * Описание: + * + * Ни один эндпоинт не требует аутентификации. Любой неавторизованный пользователь может регистрировать аккаунты, записывать сессии от имени любого пользователя, просматривать профили и аналитику, экспортировать отчёты и отправлять webhook-уведомления. Отсутствует какой-либо механизм контроля доступа. + * + * Шаги воспроизведения: + * 1. Любой пользователь без учётных данных может выполнить: + * POST /register?userId=admin&userName=Hacker + * → "User registered: true" + * 2. POST /recordSession?userId=admin&loginTime=2025-01-01T00:00:00&logoutTime=2025-12-31T23:59:59 + * → "Session recorded" (запись сессии от имени "admin") + * 3. GET /exportReport?userId=admin&filename=admin_report.txt + * → Экспорт отчёта без авторизации + * 4. Ожидаемый результат: 401 Unauthorized + * Фактический результат: успешное выполнение всех операций + * + * Влияние: + * + * Полное отсутствие контроля доступа позволяет любому пользователю интернета читать данные всех пользователей, манипулировать чужими учётными записями и аналитикой, проводить все описанные выше атаки. + * + * Внедрить механизм аутентификации (JWT, API-ключи, OAuth2). + * Добавить авторизацию: пользователь может управлять только своими данными. + * Использовать middleware/before-handler в Javalin для проверки токенов. + * Реализовать принцип наименьших привилегий. + */ +public class AuthTest extends AbstractTest { + + @Test + @DisplayName("[SECURITY] Неаутентифицированный запрос к /userProfile должен возвращать 401") + void testUnauthenticatedAccessDenied() throws Exception { + // Act — запрос без токена аутентификации + HttpResponse response = send("GET", "/userProfile?userId=test1"); + + // Assert + assertEquals(401, response.statusCode(), + "Запрос без аутентификации должен быть отклонён с кодом 401"); + } + +} diff --git a/src/test/java/ru/itmo/testing/lab4/pentest/InformationDisclosureTest.java b/src/test/java/ru/itmo/testing/lab4/pentest/InformationDisclosureTest.java new file mode 100644 index 0000000..a1168d8 --- /dev/null +++ b/src/test/java/ru/itmo/testing/lab4/pentest/InformationDisclosureTest.java @@ -0,0 +1,60 @@ +package ru.itmo.testing.lab4.pentest; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpResponse; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +/** + * Компонент - POST /recordSession, GET /monthlyActivity — UserAnalyticsController.java + * Тип - Information Exposure Through Error Message + * CWE - CWE-209 — Generation of Error Message Containing Sensitive Information + * CVSS v3.1 - 5.3 MEDIUM (AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N) + * Статус - Confirmed + * + * Описание: + * + * При возникновении исключений в эндпоинтах /recordSession, /monthlyActivity и /notify сообщение ошибки Java-исключения (e.getMessage()) передаётся напрямую в HTTP-ответ клиенту. Это раскрывает информацию о внутренней реализации: используемые библиотеки, форматы данных, структуру stack trace. + * + * Шаги воспроизведения: + * 1. POST /recordSession?userId=test&loginTime=INVALID&logoutTime=INVALID + * → 400 "Invalid data: Text 'INVALID' could not be parsed at index 0" + * 2. GET /monthlyActivity?userId=test&month=INVALID + * → 400 "Invalid data: Text 'INVALID' could not be parsed at index 0" + * 3. POST /notify?userId=test&callbackUrl=not_a_url + * → 500 "Notification failed: no protocol: not_a_url" + * 4. Ожидаемый результат: обобщённое сообщение "Invalid input format" + * Фактический результат: раскрыт парсер Java, формат ожидаемых данных + * + * Влияние: + * Атакующий получает информацию о технологическом стеке (Java, java.time парсер), что облегчает подбор целевых эксплойтов. Сообщения ошибок могут раскрыть пути файлов, имена классов, детали конфигурации. + * + * Рекомендации по устранению: + * Возвращать обобщённые сообщения об ошибках: "Invalid date format. Expected: yyyy-MM-ddTHH:mm:ss". + * Логировать детали ошибки на стороне сервера (в лог-файл), но не отдавать клиенту. + * Добавить глобальный обработчик исключений в Javalin: + */ +public class InformationDisclosureTest extends AbstractTest { + + @Test + @DisplayName("[SECURITY] Сообщения об ошибках не должны раскрывать внутреннюю реализацию") + void testErrorMessageDoesNotLeakInternals() throws Exception { + // Arrange + send("POST", "/register?userId=info_test&userName=TestUser"); + + // Act + HttpResponse response = send("POST", + "/recordSession?userId=info_test&loginTime=INVALID&logoutTime=INVALID"); + + // Assert + assertEquals(400, response.statusCode()); + assertFalse(response.body().contains("java.time"), + "Сообщение об ошибке не должно содержать имена Java-классов"); + assertFalse(response.body().contains("could not be parsed"), + "Сообщение об ошибке не должно содержать детали парсинга"); + } + +} diff --git a/src/test/java/ru/itmo/testing/lab4/pentest/NegativeSessionDuration.java b/src/test/java/ru/itmo/testing/lab4/pentest/NegativeSessionDuration.java new file mode 100644 index 0000000..19308ab --- /dev/null +++ b/src/test/java/ru/itmo/testing/lab4/pentest/NegativeSessionDuration.java @@ -0,0 +1,57 @@ +package ru.itmo.testing.lab4.pentest; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpResponse; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Компонент - POST /recordSession — UserAnalyticsController.java + UserAnalyticsService.java + * Тип - Improper Input Validation / Business Logic Error + * CWE - CWE-20 — Improper Input Validation + * CVSS v3.1 - 5.3 MEDIUM (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N) + * Статус - Confirmed + * + * Описание: + * + * Эндпоинт /recordSession не проверяет, что logoutTime позже loginTime. + * Запись сессии с logoutTime < loginTime успешно сохраняется, приводя к отрицательным значениям длительности. + * Это искажает метрики активности: суммарное время может стать отрицательным, что нарушает бизнес-логику и целостность аналитических данных. + * + * Шаги воспроизведения: + * 1. POST /register?userId=neg_test&userName=NegUser + * → "User registered: true" + * 2. POST /recordSession?userId=neg_test&loginTime=2025-01-01T10:00:00&logoutTime=2025-01-01T08:00:00 + * → "Session recorded" (logoutTime на 2 часа раньше loginTime!) + * 3. GET /totalActivity?userId=neg_test + * → "Total activity: -120 minutes" + * 4. Ожидаемый результат: ошибка 400 — logoutTime должен быть после loginTime + * Фактический результат: сессия записана, активность отрицательная + * + * Влияние: + * Атакующий может намеренно искажать аналитику, обнуляя или делая отрицательной активность любого пользователя. Может влиять на бизнес-решения, основанные на этих метриках. + * + * Рекомендации по устранению: + * 1) Добавить валидацию в recordSession + * 2) Добавить проверку на разумный максимум длительности сессии (например, не более 24 часов) + */ +public class NegativeSessionDuration extends AbstractTest { + + @Test + @DisplayName("[SECURITY] Запись сессии с logoutTime < loginTime должна быть отклонена") + void testNegativeSessionDuration() throws Exception { + // Arrange + send("POST", "/register?userId=neg_dur&userName=NegUser"); + + // Act + HttpResponse response = send("POST", + "/recordSession?userId=neg_dur&loginTime=2025-01-01T10:00:00&logoutTime=2025-01-01T08:00:00"); + + // Assert + assertEquals(400, response.statusCode(), + "Сессия с logoutTime раньше loginTime должна быть отклонена"); + } + +} diff --git a/src/test/java/ru/itmo/testing/lab4/pentest/PathTraversal.java b/src/test/java/ru/itmo/testing/lab4/pentest/PathTraversal.java new file mode 100644 index 0000000..c96d955 --- /dev/null +++ b/src/test/java/ru/itmo/testing/lab4/pentest/PathTraversal.java @@ -0,0 +1,63 @@ +package ru.itmo.testing.lab4.pentest; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpResponse; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +/** + * Компонент - GET /exportReport — UserAnalyticsController.java, строки ~120-132 + * Тип - Path Traversal / Arbitrary File Write + * CWE - CWE-22 — Improper Limitation of a Pathname to a Restricted Directory + * CVSS v3.1 - 8.6 HIGH (AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:H/A:N) + * Статус - Confirmed + * + * Описание: + * + * Эндпоинт /exportReport принимает параметр filename и конкатенирует его напрямую с базовой директорией /tmp/reports/ + * для формирования пути файла: new File(REPORTS_BASE_DIR + filename). + * Параметр не проверяется на наличие последовательностей обхода пути (../). + * Атакующий может записать произвольный файл в любую доступную директорию на сервере. + * + * Шаги воспроизведения: + * 1. POST /register?userId=pathuser&userName=Mallory + * → "User registered: true" + * 2. GET /exportReport?userId=pathuser&filename=../../../tmp/evil.txt + * → "Report saved to: /tmp/reports/../../../tmp/evil.txt" + * 3. Проверить на сервере: cat /tmp/evil.txt + * → "Report for user: Mallory\nTotal activity: 0 min" + * 4. Ожидаемый результат: ошибка 400 или запись только в /tmp/reports/ + * Фактический результат: файл записан по произвольному пути /tmp/evil.txt + * + * Влияние: + * Атакующий может перезаписать конфигурационные файлы, создать cron-задачу для выполнения произвольного кода, записать SSH-ключ в ~/.ssh/authorized_keys, перезаписать скрипты инициализации — в итоге добиться полного RCE (Remote Code Execution) на сервере. + * + * + * Рекомендации по устранению: + * 1) Канонизировать путь и проверить, что он начинается с REPORTS_BASE_DIR * 2) Использовать шаблонизатор с автоэкранированием (Thymeleaf, FreeMarker). + * 2) Ограничить допустимые символы в filename регулярным выражением (только [a-zA-Z0-9._-]) + * 3) Запретить символы /, \, .. в имени файла. + */ +public class PathTraversal extends AbstractTest { + + @Test + @DisplayName("[SECURITY] Path Traversal: filename с ../ не должен записывать файлы за пределами REPORTS_BASE_DIR") + void testPathTraversalInExportReport() throws Exception { + // Arrange + send("POST", "/register?userId=pt_test&userName=TestUser"); + + // Act + HttpResponse response = send("GET", + "/exportReport?userId=pt_test&filename=" + enc("../../../tmp/traversal_test.txt")); + + // Assert — сервер должен отклонить запрос + assertEquals(400, response.statusCode(), + "Запрос с path traversal в filename должен быть отклонён с кодом 400"); + assertFalse(new java.io.File("/tmp/traversal_test.txt").exists(), + "Файл НЕ должен быть создан за пределами директории отчётов"); + } + +} diff --git a/src/test/java/ru/itmo/testing/lab4/pentest/RateLimitTest.java b/src/test/java/ru/itmo/testing/lab4/pentest/RateLimitTest.java new file mode 100644 index 0000000..aad1368 --- /dev/null +++ b/src/test/java/ru/itmo/testing/lab4/pentest/RateLimitTest.java @@ -0,0 +1,67 @@ +package ru.itmo.testing.lab4.pentest; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpResponse; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Компонент - Все эндпоинты — UserAnalyticsController.java + * Тип - Denial of Service / Resource Exhaustion + * CWE - CWE-770 — Allocation of Resources Without Limits or Throttling + * CVSS v3.1 - 5.3 MEDIUM (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L) + * Статус - Confirmed + * + * Описание: + * + * Отсутствует ограничение на количество запросов от одного клиента. + * Атакующий может создать неограниченное количество пользователей, записать неограниченное количество сессий, вызвать заполнение памяти (HashMap без лимитов), + * массово создавать файлы через /exportReport, блокировать потоки через медленный callbackUrl в /notify + * + * 1. Скрипт массовой регистрации: + * for i in $(seq 1 100000); do + * curl -X POST "http://localhost:7000/register?userId=user_$i&userName=Spam" + * done + * → Все 100000 пользователей успешно зарегистрированы, память сервера растёт + * + * 2. Массовая запись сессий: + * for i in $(seq 1 100000); do + * curl -X POST "http://localhost:7000/recordSession?userId=user_1&loginTime=2025-01-01T00:00:00&logoutTime=2025-01-01T01:00:00" + * done + * → 100000 сессий записаны для одного пользователя + * + * 3. Ожидаемый результат: после N запросов → 429 Too Many Requests + * Фактический результат: все запросы обработаны без ограничений + * + * Влияние: + * Исчерпание оперативной памяти сервера (OOM), деградация производительности, полная недоступность сервиса. + * + * Рекомендации по устранению: + * Добавить rate limiting middleware (например, через Javalin plugin или bucket4j). + * Ограничить максимальное количество пользователей и сессий на пользователя. + * Добавить таймауты и ограничения на размер запросов. + * Использовать reverse proxy (nginx) с ограничением limit_req + */ +public class RateLimitTest extends AbstractTest { + + @Test + @DisplayName("[SECURITY] Массовые запросы должны ограничиваться rate limiter-ом") + void testRateLimiting() throws Exception { + // Act — отправить 100 запросов подряд + int rateLimitedCount = 0; + for (int i = 0; i < 100; i++) { + HttpResponse response = send("POST", + "/register?userId=rate_" + i + "&userName=Test"); + if (response.statusCode() == 429) { + rateLimitedCount++; + } + } + + // Assert — часть запросов должна быть ограничена + assertTrue(rateLimitedCount > 0, + "При массовых запросах сервер должен возвращать 429 Too Many Requests"); + } + +} diff --git a/src/test/java/ru/itmo/testing/lab4/pentest/ServerSideRequestForgeryTest.java b/src/test/java/ru/itmo/testing/lab4/pentest/ServerSideRequestForgeryTest.java new file mode 100644 index 0000000..5c48046 --- /dev/null +++ b/src/test/java/ru/itmo/testing/lab4/pentest/ServerSideRequestForgeryTest.java @@ -0,0 +1,106 @@ +package ru.itmo.testing.lab4.pentest; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpResponse; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +/** + * Компонент - POST /notify — UserAnalyticsController.java, строки ~138-152 + * Тип - Server-Side Request Forgery (SSRF) + * CWE - CWE-918 — Server-Side Request Forgery + * CVSS v3.1 - 9.1 CRITICAL (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N) + * Статус - Confirmed + * + * Описание: + * + * Эндпоинт /notify принимает параметр callbackUrl и выполняет HTTP-запрос к указанному URL без какой-либо валидации. + * Атакующий может указать URL, указывающий на внутренние сервисы (http://localhost, http://169.254.169.254), + * локальные файлы (file:///etc/passwd) или произвольные адреса внутренней сети. + * Ответ от внутреннего ресурса возвращается атакующему в полном объёме. + * + * Шаги воспроизведения: + * 1. POST /register?userId=ssrf_user&userName=Attacker + * → "User registered: true" + * + * 2. POST /notify?userId=ssrf_user&callbackUrl=file:///etc/passwd + * → "Notification sent. Response: root:x:0:0:root:/root:/bin/bash..." + * Ожидаемый результат: ошибка 400 — недопустимая схема URL + * Фактический результат: содержимое /etc/passwd возвращено атакующему + * + * 3. POST /notify?userId=ssrf_user&callbackUrl=http://169.254.169.254/latest/meta-data/ + * → "Notification sent. Response: ami-id\nami-launch-index..." + * Ожидаемый результат: ошибка 400 — внутренний адрес запрещён + * Фактический результат: метаданные облачного окружения раскрыты + * + * 4. POST /notify?userId=ssrf_user&callbackUrl=http://localhost:7000/inactiveUsers?days=0 + * → "Notification sent. Response: [...]" + * Фактический результат: обращение к внутренним эндпоинтам + * + * Влияние: + * Чтение произвольных файлов с сервера (file://) + * Доступ к облачным метаданным (AWS/GCP/Azure) → получение временных креденшелов с полным доступом к облачной инфраструктуре + * Сканирование внутренней сети, обнаружение и эксплуатация внутренних сервисов + * Обход firewall, доступ к БД и другим бэкенд-компонентам + * + * Рекомендации по устранению: + * Реализовать whitelist разрешённых доменов/хостов для callbackUrl. + * Разрешить только схемы http и https (запретить file://, ftp://, gopher:// и т.д.). + * Проверять resolved IP-адрес: запретить приватные диапазоны (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), loopback (127.0.0.0/8), link-local (169.254.0.0/16). + * Использовать прокси для исходящих запросов с ограничениями на целевые адреса. + * Не возвращать тело ответа от внутренних ресурсов пользователю. + */ +public class ServerSideRequestForgeryTest extends AbstractTest { + + @Test + @DisplayName("[SECURITY] SSRF: callbackUrl с file:// схемой должен быть отклонён") + void testSsrfFileProtocol() throws Exception { + // Arrange + send("POST", "/register?userId=ssrf_test&userName=TestUser"); + + // Act + HttpResponse response = send("POST", + "/notify?userId=ssrf_test&callbackUrl=" + enc("file:///etc/passwd")); + + // Assert + assertNotEquals(200, response.statusCode(), + "Запрос с file:// схемой не должен быть успешно обработан"); + assertFalse(response.body().contains("root:"), + "Содержимое локальных файлов не должно возвращаться в ответе"); + } + + @Test + @DisplayName("[SECURITY] SSRF: callbackUrl с внутренним IP (169.254.169.254) должен быть отклонён") + void testSsrfCloudMetadata() throws Exception { + // Arrange + send("POST", "/register?userId=ssrf_test2&userName=TestUser"); + + // Act + HttpResponse response = send("POST", + "/notify?userId=ssrf_test2&callbackUrl=" + enc("http://169.254.169.254/latest/meta-data/")); + + // Assert + assertEquals(400, response.statusCode(), + "Запрос к cloud metadata endpoint должен быть отклонён"); + } + + @Test + @DisplayName("[SECURITY] SSRF: callbackUrl с localhost должен быть отклонён") + void testSsrfLocalhost() throws Exception { + // Arrange + send("POST", "/register?userId=ssrf_test3&userName=TestUser"); + + // Act + HttpResponse response = send("POST", + "/notify?userId=ssrf_test3&callbackUrl=" + enc("http://127.0.0.1:7000/inactiveUsers?days=0")); + + // Assert + assertEquals(400, response.statusCode(), + "Запрос к localhost должен быть отклонён"); + } + +} diff --git a/src/test/java/ru/itmo/testing/lab4/pentest/XssPentestTest.java b/src/test/java/ru/itmo/testing/lab4/pentest/XssPentestTest.java index 83f306d..d6c5d5b 100644 --- a/src/test/java/ru/itmo/testing/lab4/pentest/XssPentestTest.java +++ b/src/test/java/ru/itmo/testing/lab4/pentest/XssPentestTest.java @@ -48,10 +48,7 @@ * ============================================================================= */ @TestMethodOrder(MethodOrderer.OrderAnnotation.class) -class XssPentestTest { - - private static final int TEST_PORT = 7777; - private static final String BASE_URL = "http://localhost:" + TEST_PORT; +class XssPentestTest extends AbstractTest { // Типичные XSS-пейлоады private static final String PAYLOAD_BASIC = ""; @@ -61,18 +58,6 @@ class XssPentestTest { private static Javalin app; private static HttpClient http; - @BeforeAll - static void startServer() { - app = UserAnalyticsController.createApp(); - app.start(TEST_PORT); - http = HttpClient.newHttpClient(); - } - - @AfterAll - static void stopServer() { - app.stop(); - } - // ------------------------------------------------------------------------- // RECONNAISSANCE: убеждаемся, что эндпоинт вообще отдаёт HTML // ------------------------------------------------------------------------- @@ -175,21 +160,4 @@ void missingUserIdReturns400() throws Exception { HttpResponse response = send("GET", "/userProfile"); assertEquals(400, response.statusCode()); } - - // ------------------------------------------------------------------------- - // Вспомогательный метод - // ------------------------------------------------------------------------- - - /** Кодирует значение query-параметра для безопасной передачи в URI. */ - private static String enc(String value) { - return URLEncoder.encode(value, StandardCharsets.UTF_8); - } - - private HttpResponse send(String method, String path) throws Exception { - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(BASE_URL + path)) - .method(method, HttpRequest.BodyPublishers.noBody()) - .build(); - return http.send(request, HttpResponse.BodyHandlers.ofString()); - } } diff --git a/src/test/java/ru/itmo/testing/lab4/pentest/XssTest.java b/src/test/java/ru/itmo/testing/lab4/pentest/XssTest.java new file mode 100644 index 0000000..36a2894 --- /dev/null +++ b/src/test/java/ru/itmo/testing/lab4/pentest/XssTest.java @@ -0,0 +1,63 @@ +package ru.itmo.testing.lab4.pentest; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpResponse; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Компонент - GET /userProfile — UserAnalyticsController.java, строки ~107-113 + * Тип - Stored/Reflected XSS + * CWE - CWE-79 — Improper Neutralization of Input During Web Page Generation + * CVSS v3.1 - 6.1 MEDIUM (AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N) + * Статус - Confirmed + * + * Описание: + * + * Эндпоинт /userProfile формирует HTML-страницу путём прямой конкатенации значения userName из хранилища в HTML-разметку. + * Значение userName попадает в систему через /register без какой-либо санитизации. + * Атакующий регистрирует пользователя с вредоносным именем, содержащим JavaScript-код, который выполняется в браузере жертвы при просмотре профиля. + * + * Шаги воспроизведения: + * 1. POST /register?userId=xss_victim&userName= + * → Ответ: "User registered: true" + * 2. GET /userProfile?userId=xss_victim + * → Ответ:

Profile:

... + * 3. Ожидаемый результат: спецсимволы экранированы (<script>) + * Фактический результат: тег "; + send("POST", "/register?userId=xss_test&userName=" + enc(xssPayload)); + + // Act + HttpResponse response = send("GET", "/userProfile?userId=xss_test"); + + // Assert — тело ответа НЕ должно содержать сырой