diff --git a/.idea/.name b/.idea/.name
new file mode 100644
index 0000000..786c7a2
--- /dev/null
+++ b/.idea/.name
@@ -0,0 +1 @@
+software_testing_lab_4
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 0000000..e5d35df
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 9a03a98..5112058 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -1,5 +1,6 @@
+
-
+
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index 94a25f7..35eb1dd 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 18eee9a..6d50c5c 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@
+[](https://classroom.github.com/a/NSTTkgmb)
# Лабораторная работа №4 — Анализ и тестирование безопасности веб-приложения
## Цель
@@ -40,12 +41,12 @@
Заполните таблицу активов системы:
-| Актив | Тип | Ценность | Примечание |
-|-------|-----|----------|------------|
-| Данные пользователей (userId, userName) | Данные | ? | |
-| Данные о сессиях (время входа/выхода) | Данные | ? | |
-| Файловая система сервера | Инфраструктура | ? | |
-| Внутренняя сеть / метаданные окружения | Инфраструктура | ? | |
+| Актив | Тип | Ценность | Примечание |
+|-------|-----|-------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Данные пользователей (userId, userName) | Данные | Высокая | userId используется как первичный ключ для доступа ко всем другим данным пользователя. userName может содержать XSS-векторы |
+| Данные о сессиях (время входа/выхода) | Данные | Средняя | Поведенческие данные - позволяют анализировать активность, вычислять неактивных пользователей, строить графики активности. При утечке могут раскрыть режим работы пользователя. |
+| Файловая система сервера | Инфраструктура | Критическая | Эндпоинт /exportReport принимает параметр filename - прямая угроза Path Traversal. Возможно чтение конфигураций, исходного кода, секретов, SSH-ключей, данных других пользователей. |
+| Внутренняя сеть / метаданные окружения | Инфраструктура | Критическая | Эндпоинт /notify принимает callbackUrl - угроза SSRF (Server-Side Request Forgery). Атакующий может сканировать внутреннюю сеть, атаковать внутренние сервисы |
> **Вопрос для размышления:** какие из активов наиболее критичны и почему?
@@ -55,14 +56,14 @@
Проведите базовое моделирование угроз по методологии **STRIDE**:
-| Категория угрозы | Расшифровка | Применимо к этому приложению? |
-|------------------|------------------------|-------------------------------|
-| **S**poofing | Подмена идентификации | ? |
-| **T**ampering | Модификация данных | ? |
-| **R**epudiation | Отказ от авторства | ? |
-| **I**nformation Disclosure | Утечка данных | ? |
-| **D**enial of Service | Отказ в обслуживании | ? |
-| **E**levation of Privilege | Повышение привилегий | ? |
+| Категория угрозы | Расшифровка | Применимо к этому приложению? | |
+|------------------|------------------------|-------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| **S**poofing | Подмена идентификации | Да | userId передаётся как GET/POST параметр - нет аутентификации, нет сессий, нет токенов. Злоумышленник может подставить любой userId и получить данные другого пользователя. |
+| **T**ampering | Модификация данных | Да | Отсутствует контроль целостности. Через userId в запросах можно модифицировать сессии (/recordSession) или экспортировать отчёты других пользователей (/exportReport?userId=). |
+| **R**epudiation | Отказ от авторства | Да | Нет логирования действий. Невозможно доказать, кто именно создал сессию или запросил отчёт. Атакующий может отрицать свои действия. |
+| **I**nformation Disclosure | Утечка данных | Да | Множество векторов: Path Traversal (/exportReport?filename=), SSRF (/notify?callbackUrl=), отсутствие ACL на userId, возможные ошибки с выводом stack traces. |
+| **D**enial of Service | Отказ в обслуживании | Да | Нет ограничений на количество запросов, размер данных, сложность запросов. /recordSession может заспамить миллионами сессий. /exportReport может открыть огромные файлы. |
+| **E**levation of Privilege | Повышение привилегий | Частично | Нет ролевой модели (все пользователи равны). Однако через Path Traversal или SSRF атакующий может получить доступ к файлам сервера -> перейти от «пользователь» к «сервер» / «внутренняя сеть». |
Для каждой применимой угрозы укажите:
- **Источник угрозы** (кто/что может её реализовать)
@@ -71,6 +72,88 @@
---
+### Детализация угроз
+
+#### Spoofing (подмена идентификации)
+
+| Поле | Значение |
+|------|----------|
+| **Источник угрозы** | Внешний неаутентифицированный злоумышленник |
+| **Поверхность атаки** | Все эндпоинты, принимающие `userId`: `/register`, `/recordSession`, `/totalActivity`, `/monthlyActivity`, `/userProfile`, `/exportReport`, `/notify` |
+| **Потенциальный ущерб** | Доступ к чужим данным, модификация чужих сессий, экспорт отчётов других пользователей. Любой может представиться любым `userId`. |
+
+---
+
+#### Tampering (модификация данных)
+
+| Поле | Значение |
+|------|----------|
+| **Источник угрозы** | Внешний злоумышленник |
+| **Поверхность атаки** | `/recordSession` (параметры `userId`, `loginTime`, `logoutTime`), `/register` (повторная регистрация того же `userId` с другим `userName`) |
+| **Потенциальный ущерб** | Подмена чужих сессий, искажение аналитики активности, засорение базы данных ложными записями, нарушение целостности отчётности. |
+
+---
+
+#### Repudiation (отказ от авторства)
+
+| Поле | Значение |
+|------|----------------------------------------------------------------------------------------------------------------------------|
+| **Источник угрозы** | Любой пользователь (злонамеренный или легальный, желающий скрыть свои действия) |
+| **Поверхность атаки** | Все эндпоинты - отсутствие аудита (логирования запросов с идентификацией) |
+| **Потенциальный ущерб** | Невозможно расследовать инцидент безопасности. Администратор не может определить, кто использовал Path Traversal или SSRF. |
+
+---
+
+#### Information Disclosure: Path Traversal
+
+| Поле | Значение |
+|------|----------|
+| **Источник угрозы** | Внешний злоумышленник |
+| **Поверхность атаки** | `GET /exportReport?userId=any&filename=` |
+| **Потенциальный ущерб** | Чтение любых файлов на сервере: `/etc/passwd`, конфигурационные файлы с паролями БД, исходный код приложения, JWT-секреты, SSH-ключи, переменные окружения. |
+
+---
+
+#### Information Disclosure: SSRF
+
+| Поле | Значение |
+|------|----------|
+| **Источник угрозы** | Внешний злоумышленник |
+| **Поверхность атаки** | `POST /notify?userId=any&callbackUrl=` |
+| **Потенциальный ущерб** | Сканирование внутренней сети, атаки на внутренние сервисы. |
+
+---
+
+#### Information Disclosure: XSS (Cross-Site Scripting)
+
+| Поле | Значение |
+|------|----------|
+| **Источник угрозы** | Внешний злоумышленник |
+| **Поверхность атаки** | `POST /register` (параметр `userName`), затем `GET /userProfile` (рендеринг имени без экранирования) |
+| **Потенциальный ущерб** | Выполнение произвольного JavaScript в браузере жертвы: кража сессий, фишинг, подделка запросов от имени пользователя. |
+
+---
+
+#### Denial of Service (отказ в обслуживании)
+
+| Поле | Значение |
+|------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| **Источник угрозы** | Внешний злоумышленник или ботнет |
+| **Поверхность атаки** | Любой эндпоинт, особенно: `/recordSession` (массовая вставка сессий), `/exportReport` (чтение огромных файлов), `/totalActivity` (тяжёлые агрегатные запросы) |
+| **Потенциальный ущерб** | Переполнение базы данных миллионами записей, блокировка CPU/IO, исчерпание памяти, падение сервера, невозможность легальным пользователям получить доступ к приложению. |
+
+---
+
+#### Elevation of Privilege (повышение привилегий)
+
+| Поле | Значение |
+|------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| **Источник угрозы** | Внешний злоумышленник |
+| **Поверхность атаки** | Path Traversal (`/exportReport`) -> чтение конфигурационных файлов с паролями/токенами -> доступ к БД или внутренним сервисам. SSRF (`/notify`) -> доступ к внутренней сети и метаданным облака -> компрометация инфраструктуры. |
+| **Потенциальный ущерб** | Переход от «неаутентифицированный пользователь» к «привилегии сервера», «доступ к базе данных», «контроль над облачной средой». Полный захват приложения и инфраструктуры. |
+
+---
+
### Этап 3 — Ручное тестирование
Исследуйте каждый эндпоинт вручную — с помощью `curl`, Postman, Burp Suite или браузера.
@@ -99,6 +182,24 @@ curl "http://localhost:7000/userProfile?userId=evil"
> Данный пример — лишь отправная точка. Исследуйте **все** эндпоинты самостоятельно.
+## Сводная таблица найденных уязвимостей
+
+| № | Эндпоинт | Уязвимость | CWE |
+|---|----------|-----------|-----|
+| 1 | `/register` + `/userProfile` | Stored XSS | [CWE-79](https://cwe.mitre.org/data/definitions/79.html) |
+| 2 | Все эндпоинты | Отсутствие аутентификации | [CWE-306](https://cwe.mitre.org/data/definitions/306.html) |
+| 3 | Все эндпоинты | Отсутствие rate limiting | [CWE-770](https://cwe.mitre.org/data/definitions/770.html) |
+| 4 | `/recordSession` | IDOR (Spoofing) | [CWE-639](https://cwe.mitre.org/data/definitions/639.html) |
+| 5 | `/totalActivity` | IDOR (Spoofing) | [CWE-639](https://cwe.mitre.org/data/definitions/639.html) |
+| 6 | `/monthlyActivity` | IDOR (Spoofing) | [CWE-639](https://cwe.mitre.org/data/definitions/639.html) |
+| 7 | `/exportReport` | Path Traversal (запись файлов) | [CWE-22](https://cwe.mitre.org/data/definitions/22.html) |
+| 8 | `/exportReport` | Directory Traversal / перезапись файлов | [CWE-73](https://cwe.mitre.org/data/definitions/73.html) |
+| 9 | `/notify` | SSRF (Server-Side Request Forgery) | [CWE-918](https://cwe.mitre.org/data/definitions/918.html) |
+| 10 | `/notify` | SSRF через file:// протокол (чтение файлов) | [CWE-918](https://cwe.mitre.org/data/definitions/918.html) |
+| 11 | `/inactiveUsers` | Information Disclosure (ошибки парсинга) | [CWE-209](https://cwe.mitre.org/data/definitions/209.html) |
+| 12 | `/monthlyActivity` | Information Disclosure (ошибки парсинга) | [CWE-209](https://cwe.mitre.org/data/definitions/209.html) |
+| 13 | `/recordSession` | Information Disclosure (ошибки парсинга) | [CWE-209](https://cwe.mitre.org/data/definitions/209.html) |
+
---
### Этап 4 — Статический анализ с Semgrep
diff --git a/build.gradle b/build.gradle
index fb0a8f7..2b0040a 100644
--- a/build.gradle
+++ b/build.gradle
@@ -12,6 +12,7 @@ repositories {
dependencies {
implementation 'io.javalin:javalin:6.3.0'
implementation 'org.slf4j:slf4j-simple:2.0.13'
+ implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.2'
testImplementation platform('org.junit:junit-bom:6.0.0')
testImplementation 'org.junit.jupiter:junit-jupiter'
diff --git a/semgrep-report.json b/semgrep-report.json
new file mode 100644
index 0000000..298c510
--- /dev/null
+++ b/semgrep-report.json
@@ -0,0 +1 @@
+{"version":"1.159.0","results":[],"errors":[],"paths":{"scanned":["src\\main\\java\\ru\\itmo\\testing\\lab4\\controller\\UserAnalyticsController.java","src\\main\\java\\ru\\itmo\\testing\\lab4\\Main.java","src\\main\\java\\ru\\itmo\\testing\\lab4\\profilingdemo\\ProfilingDemoMain.java","src\\main\\java\\ru\\itmo\\testing\\lab4\\profilingdemo\\ProfilingWorkload.java","src\\main\\java\\ru\\itmo\\testing\\lab4\\service\\UserAnalyticsService.java","src\\main\\java\\ru\\itmo\\testing\\lab4\\service\\UserStatusService.java"]},"time":{"rules":[],"rules_parse_time":0.12845182418823242,"profiling_times":{"config_time":2.1372992992401123,"core_time":0.6763730049133301,"ignores_time":0.00011754035949707031,"total_time":2.841381549835205},"parsing_time":{"total_time":0.0,"per_file_time":{"mean":0.0,"std_dev":0.0},"very_slow_stats":{"time_ratio":0.0,"count_ratio":0.0},"very_slow_files":[]},"scanning_time":{"total_time":0.27506208419799805,"per_file_time":{"mean":0.04584368069966634,"std_dev":0.00022842930818316946},"very_slow_stats":{"time_ratio":0.0,"count_ratio":0.0},"very_slow_files":[]},"matching_time":{"total_time":0.0,"per_file_and_rule_time":{"mean":0.0,"std_dev":0.0},"very_slow_stats":{"time_ratio":0.0,"count_ratio":0.0},"very_slow_rules_on_files":[]},"tainting_time":{"total_time":0.0,"per_def_and_rule_time":{"mean":0.0,"std_dev":0.0},"very_slow_stats":{"time_ratio":0.0,"count_ratio":0.0},"very_slow_rules_on_defs":[]},"fixpoint_timeouts":[],"prefiltering":{"project_level_time":0.0,"file_level_time":0.0,"rules_with_project_prefilters_ratio":0.0,"rules_with_file_prefilters_ratio":0.95,"rules_selected_ratio":0.10833333333333334,"rules_matched_ratio":0.10833333333333334},"targets":[],"total_bytes":0,"max_memory_bytes":208133696},"engine_requested":"OSS","skipped_rules":[],"profiling_results":[]}
\ No newline at end of file
diff --git a/src/test/java/ru/itmo/testing/lab4/pentest/AuthenticationPentestTest.java b/src/test/java/ru/itmo/testing/lab4/pentest/AuthenticationPentestTest.java
new file mode 100644
index 0000000..31b2bc0
--- /dev/null
+++ b/src/test/java/ru/itmo/testing/lab4/pentest/AuthenticationPentestTest.java
@@ -0,0 +1,179 @@
+package ru.itmo.testing.lab4.pentest;
+
+import io.javalin.Javalin;
+import org.junit.jupiter.api.*;
+import ru.itmo.testing.lab4.controller.UserAnalyticsController;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * =============================================================================
+ * ПЕНТЕСТ-ОТЧЁТ: CWE-306 - Отсутствие аутентификации
+ * =============================================================================
+ *
+ * Компонент: Все эндпоинты (GET /totalActivity, POST /recordSession, GET /monthlyActivity, и т.д.)
+ * 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
+ *
+ * ОПИСАНИЕ:
+ * Приложение не проверяет аутентификацию пользователя перед выполнением любых действий.
+ * Любой внешний злоумышленник может выполнять запросы от имени любого пользователя,
+ * подставляя произвольный userId в query-параметры.
+ *
+ * ВЕКТОР АТАКИ:
+ * 1. Злоумышленник регистрирует своего пользователя или находит существующего
+ * 2. Выполняет запрос без какой-либо аутентификации:
+ * GET /totalActivity?userId=alice
+ * → Сервер возвращает данные пользователя alice без проверки прав.
+ *
+ * ВЛИЯНИЕ:
+ * - Получение данных любого пользователя
+ * - Запись фейковых сессий от имени любого пользователя
+ * - Эксплуатация всех остальных уязвимостей без ограничений
+ *
+ * МЕРЫ ЗАЩИТЫ:
+ * - Добавить механизм аутентификации (JWT, Session, OAuth2)
+ * - Добавить before-обработчик для проверки токена
+ * - Использовать userId из аутентифицированного контекста, а не из параметров запроса
+ * =============================================================================
+ */
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+class AuthenticationPentestTest {
+
+ 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();
+ }
+
+ @Test
+ @Order(1)
+ @DisplayName("[RECON] Регистрация пользователя для тестирования")
+ void registerTestUser() throws Exception {
+ HttpResponse response = send("POST", "/register?userId=alice&userName=Alice");
+ assertEquals(200, response.statusCode());
+ assertTrue(response.body().contains("User registered"));
+ }
+
+ @Test
+ @Order(2)
+ @DisplayName("[RECON] /totalActivity доступен без аутентификации - УЯЗВИМОСТЬ ПОДТВЕРЖДЕНА")
+ void totalActivityAccessibleWithoutAuth() throws Exception {
+ send("POST", "/recordSession?userId=alice&loginTime=2024-01-01T09:00:00&logoutTime=2024-01-01T17:00:00");
+
+ HttpResponse response = send("GET", "/totalActivity?userId=alice");
+
+ assertEquals(200, response.statusCode(),
+ "УЯЗВИМОСТЬ: /totalActivity доступен без аутентификации, должен быть защищён");
+ assertTrue(response.body().contains("Total activity"),
+ "Данные пользователя возвращаются без аутентификации");
+ }
+
+ @Test
+ @Order(3)
+ @DisplayName("[RECON] /recordSession доступен без аутентификации - УЯЗВИМОСТЬ ПОДТВЕРЖДЕНА")
+ void recordSessionAccessibleWithoutAuth() throws Exception {
+ HttpResponse response = send("POST",
+ "/recordSession?userId=alice&loginTime=2024-01-02T09:00:00&logoutTime=2024-01-02T17:00:00");
+
+ assertEquals(200, response.statusCode(),
+ "УЯЗВИМОСТЬ: /recordSession доступен без аутентификации, должен быть защищён");
+ assertTrue(response.body().contains("Session recorded"),
+ "Сессия записана без аутентификации");
+ }
+
+ @Test
+ @Order(4)
+ @DisplayName("[RECON] /monthlyActivity доступен без аутентификации - УЯЗВИМОСТЬ ПОДТВЕРЖДЕНА")
+ void monthlyActivityAccessibleWithoutAuth() throws Exception {
+ HttpResponse response = send("GET", "/monthlyActivity?userId=alice&month=2024-01");
+
+ assertEquals(200, response.statusCode(),
+ "УЯЗВИМОСТЬ: /monthlyActivity доступен без аутентификации, должен быть защищён");
+ }
+
+ @Test
+ @Order(5)
+ @DisplayName("[RECON] /exportReport доступен без аутентификации - УЯЗВИМОСТЬ ПОДТВЕРЖДЕНА")
+ void exportReportAccessibleWithoutAuth() throws Exception {
+ HttpResponse response = send("GET", "/exportReport?userId=alice&filename=test.txt");
+
+ assertEquals(200, response.statusCode(),
+ "УЯЗВИМОСТЬ: /exportReport доступен без аутентификации, должен быть защищён");
+ }
+
+ @Test
+ @Order(6)
+ @DisplayName("[RECON] /notify доступен без аутентификации - УЯЗВИМОСТЬ ПОДТВЕРЖДЕНА")
+ void notifyAccessibleWithoutAuth() throws Exception {
+ HttpResponse response = send("POST",
+ "/notify?userId=alice&callbackUrl=https://httpbin.org/get");
+
+ assertEquals(200, response.statusCode(),
+ "УЯЗВИМОСТЬ: /notify доступен без аутентификации, должен быть защищён");
+ }
+
+ @Test
+ @Order(7)
+ @DisplayName("[EXPLOIT] Можно получить данные другого пользователя без аутентификации")
+ void canAccessOtherUserDataWithoutAuth() throws Exception {
+ send("POST", "/register?userId=bob&userName=Bob");
+ send("POST", "/recordSession?userId=bob&loginTime=2024-01-01T09:00:00Z&logoutTime=2024-01-01T17:00:00Z");
+
+ HttpResponse response = send("GET", "/totalActivity?userId=bob");
+
+ assertEquals(200, response.statusCode(),
+ "УЯЗВИМОСТЬ: данные пользователя bob доступны без аутентификации");
+ assertTrue(response.body().contains("Total activity"),
+ "Возвращаются данные чужого пользователя");
+ }
+
+ @Test
+ @Order(8)
+ @DisplayName("[EXPLOIT] Можно записать сессию от имени другого пользователя без аутентификации")
+ void canRecordSessionForOtherUserWithoutAuth() throws Exception {
+ HttpResponse response = send("POST",
+ "/recordSession?userId=bob&loginTime=2024-01-03T09:00:00&logoutTime=2024-01-03T17:00:00");
+
+ assertEquals(200, response.statusCode(),
+ "УЯЗВИМОСТЬ: можно записать сессию от имени другого пользователя без аутентификации");
+ assertTrue(response.body().contains("Session recorded"),
+ "Сессия записана от имени чужого пользователя");
+ }
+
+ @Test
+ @Order(9)
+ @DisplayName("[BOUNDARY] Несуществующий userId в /totalActivity возвращает 0")
+ void nonExistentUserInTotalActivity() throws Exception {
+ HttpResponse response = send("GET", "/totalActivity?userId=nonexistent");
+
+ assertTrue(response.body().contains("0 minutes") || response.body().contains("0"),
+ "Несуществующий пользователь должен возвращать 0 активности");
+ }
+
+ 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());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/itmo/testing/lab4/pentest/IdorPentestTest.java b/src/test/java/ru/itmo/testing/lab4/pentest/IdorPentestTest.java
new file mode 100644
index 0000000..6c8ed8c
--- /dev/null
+++ b/src/test/java/ru/itmo/testing/lab4/pentest/IdorPentestTest.java
@@ -0,0 +1,126 @@
+package ru.itmo.testing.lab4.pentest;
+
+import io.javalin.Javalin;
+import org.junit.jupiter.api.*;
+import ru.itmo.testing.lab4.controller.UserAnalyticsController;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * =============================================================================
+ * ПЕНТЕСТ-ОТЧЕТ: CWE-639 - IDOR (Insecure Direct Object Reference)
+ * =============================================================================
+ *
+ * Компонент: POST /recordSession, GET /totalActivity, GET /monthlyActivity
+ * CWE: CWE-639 (Authorization Bypass Through User-Controlled Key)
+ * CVSS v3.1: 5.3 (Medium) - AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
+ * Статус: CONFIRMED
+ *
+ * ОПИСАНИЕ:
+ * Параметр userId передаётся через query-параметры и не проверяется на соответствие
+ * аутентифицированному пользователю. Любой может подставить чужой userId и получить
+ * или модифицировать его данные.
+ *
+ * ВЕКТОР АТАКИ:
+ * 1. Злоумышленник узнаёт, что существует пользователь "bob"
+ * 2. Выполняет запрос с подменой userId:
+ * GET /totalActivity?userId=bob
+ * -> Сервер возвращает данные пользователя bob, хотя злоумышленник не авторизован для этого.
+ *
+ * ВЛИЯНИЕ:
+ * - Получение активности любого пользователя
+ * - Запись фейковых сессий от имени других пользователей
+ * - Искажение аналитики и отчётов
+ *
+ * Компонент: POST /recordSession, GET /monthlyActivity, GET /exportReport, POST /notify
+ * CWE: CWE-209 (Generation of Error Message Containing Sensitive Information)
+ * CVSS v3.1: 4.3 (Medium) - AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
+ * Статус: CONFIRMED
+ *
+ * ОПИСАНИЕ:
+ * При возникновении ошибок приложение возвращает детали исключений
+ * (e.getMessage()) в теле ответа, что раскрывает внутреннюю структуру
+ * приложения, имена классов, методы парсинга.
+ *
+ * ВЕКТОР АТАКИ:
+ * 1. Отправить запрос с некорректной датой:
+ * POST /recordSession?userId=alice&loginTime=invalid&logoutTime=2024-01-01T17:00:00
+ * -> Сервер возвращает: "Invalid data: Text 'invalid' could not be parsed at index 0"
+ *
+ * ВЛИЯНИЕ:
+ * - Узнать внутренние имена классов и методов
+ * - Получить информацию о структуре парсинга дат
+ * - Собрать данные для более точных атак
+ *
+ * Компонент: GET /exportReport
+ * CWE: CWE-22 (Improper Limitation of a Pathname to a Restricted Directory)
+ * CWE-73 (External Control of File Name or Path)
+ * CVSS v3.1: 7.5 (High) - AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N
+ * Статус: CONFIRMED
+ *
+ * ОПИСАНИЕ:
+ * Эндпоинт /exportReport принимает параметр filename и конкатенирует его
+ * с базовой директорией /tmp/reports/ без валидации.
+ * Злоумышленник может использовать ../ для записи файлов в произвольные директории.
+ *
+ * ВЕКТОР АТАКИ:
+ * 1. Злоумышленник регистрирует пользователя:
+ * POST /register?userId=alice&userName=Alice
+ * 2. Выполняет запрос с Path Traversal:
+ * GET /exportReport?userId=alice&filename=../../../../tmp/evil.txt
+ * -> Сервер создаёт файл /tmp/evil.txt (вне разрешённой директории)
+ *
+ * ВЛИЯНИЕ:
+ * - Перезапись существующих файлов (включая конфигурационные)
+ * - Запись вредоносных скриптов (если есть права)
+ * - Заполнение диска мусорными файлами (DoS)
+ * - Раскрытие структуры файловой системы через ошибки
+ *
+ * Компонент: Все эндпоинты (GET /totalActivity, POST /recordSession и т.д.)
+ * 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
+ *
+ * ОПИСАНИЕ:
+ * Приложение не ограничивает количество запросов от одного клиента.
+ * Злоумышленник может отправлять тысячи запросов в секунду, вызывая
+ * отказ в обслуживании (Denial of Service).
+ *
+ * ВЕКТОР АТАКИ:
+ * 1. Злоумышленник регистрирует пользователя
+ * 2. Выполняет 200 быстрых последовательных запросов подряд
+ * -> Все 200 запросов успешно обработаны, сервер перегружен.
+ *
+ * ВЛИЯНИЕ:
+ * - Перегрузка сервера CPU/IO
+ * - Исчерпание памяти
+ * - Недоступность приложения для легитимных пользователей
+ * - Заполнение базы данных миллионами фейковых сессий
+ *
+ * МЕРЫ ЗАЩИТЫ:
+ * - Внедрить rate limiting с помощью Bucket4j, Resilience4j
+ * - Ограничить количество запросов в минуту на IP или userId
+ * - Возвращать HTTP 429 Too Many Requests при превышении лимита
+ * =============================================================================
+ */
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+class RateLimitingPentestTest {
+
+ private static final int TEST_PORT = 7777;
+ private static final String BASE_URL = "http://localhost:" + TEST_PORT;
+ private static final int REQUEST_LIMIT = 100;
+ private static final int EXCEED_LIMIT = 200;
+
+ 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();
+ }
+
+ @Test
+ @Order(1)
+ @DisplayName("[RECON] Регистрация пользователя для тестирования")
+ void registerTestUser() throws Exception {
+ HttpResponse response = send("POST", "/register?userId=rate_test&userName=RateTest");
+
+ assertEquals(200, response.statusCode());
+ assertTrue(response.body().contains("User registered"),
+ "Пользователь должен успешно зарегистрироваться");
+ }
+
+ @Test
+ @Order(2)
+ @DisplayName("[EXPLOIT] Отсутствие rate limiting - можно отправить много запросов подряд")
+ void noRateLimitingMultipleRequests() throws Exception {
+ int successCount = 0;
+ long startTime = System.currentTimeMillis();
+
+ for (int i = 0; i < EXCEED_LIMIT; i++) {
+ HttpResponse response = send("GET", "/totalActivity?userId=rate_test");
+ if (response.statusCode() == 200) {
+ successCount++;
+ }
+ }
+
+ long duration = System.currentTimeMillis() - startTime;
+
+ System.out.println("Requests: " + EXCEED_LIMIT +
+ ", Success: " + successCount +
+ ", Duration: " + duration + "ms");
+
+ assertEquals(EXCEED_LIMIT, successCount,
+ "УЯЗВИМОСТЬ: все " + EXCEED_LIMIT + " запросов должны быть обработаны, " +
+ "так как rate limiting отсутствует. Обработано: " + successCount);
+ }
+
+ @Test
+ @Order(3)
+ @DisplayName("[EXPLOIT] Отсутствие rate limiting - спам сессиями")
+ void noRateLimitingSessionSpam() throws Exception {
+ int successCount = 0;
+ int sessionSpamCount = 100;
+
+ for (int i = 0; i < sessionSpamCount; i++) {
+ HttpResponse response = send("POST",
+ "/recordSession?userId=rate_test&loginTime=2024-01-01T09:00:00&logoutTime=2024-01-01T17:00:00");
+ if (response.statusCode() == 200) {
+ successCount++;
+ }
+ }
+
+ assertTrue(successCount > sessionSpamCount * 0.9,
+ "УЯЗВИМОСТЬ: спам сессиями должен ограничиваться rate limiting, " +
+ "но обработано " + successCount + " из " + sessionSpamCount + " запросов");
+ }
+
+ @Test
+ @Order(4)
+ @DisplayName("[BOUNDARY] Запросы в рамках лимита должны работать")
+ void requestsWithinLimitShouldWork() throws Exception {
+ for (int i = 0; i < REQUEST_LIMIT; i++) {
+ HttpResponse response = send("GET", "/totalActivity?userId=rate_test");
+
+ assertNotEquals(500, response.statusCode(),
+ "Сервер не должен падать на запросе " + i);
+ }
+ }
+
+ 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());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/itmo/testing/lab4/pentest/SsrfPentestTest.java b/src/test/java/ru/itmo/testing/lab4/pentest/SsrfPentestTest.java
new file mode 100644
index 0000000..d077f43
--- /dev/null
+++ b/src/test/java/ru/itmo/testing/lab4/pentest/SsrfPentestTest.java
@@ -0,0 +1,194 @@
+package ru.itmo.testing.lab4.pentest;
+
+import io.javalin.Javalin;
+import org.junit.jupiter.api.*;
+import ru.itmo.testing.lab4.controller.UserAnalyticsController;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * =============================================================================
+ * ПЕНТЕСТ-ОТЧЕТ: CWE-918 - Server-Side Request Forgery (SSRF)
+ * =============================================================================
+ *
+ * ОПИСАНИЕ:
+ * Эндпоинт /notify принимает параметр callbackUrl и делает HTTP-запрос
+ * по этому URL без валидации. Злоумышленник может заставить сервер
+ * отправлять запросы к внутренним ресурсам.
+ *
+ * ВЕКТОР АТАКИ:
+ * 1. Выполнить запрос с callbackUrl на внутренний сервис:
+ * POST /notify?userId=alice&callbackUrl=...
+ * -> Сервер делает запрос к указанному URL и возвращает ответ.
+ *
+ * ВЛИЯНИЕ:
+ * - Сканирование внутренней сети
+ * - Получение метаданных облачных провайдеров (AWS/GCP/Azure)
+ * - Атака на внутренние сервисы (Redis, Elasticsearch, базы данных)
+ * - Чтение локальных файлов через file:// протокол
+ *