diff --git a/PentestReport.md b/PentestReport.md
new file mode 100644
index 0000000..bd76d0b
--- /dev/null
+++ b/PentestReport.md
@@ -0,0 +1,394 @@
+# 🛡️ Этап 5 — Pentest Report
+
+## 📊 Общее резюме
+
+В результате выявлены **критические уязвимости уровня HIGH и CRITICAL**, позволяющие:
+
+- выполнять произвольный JavaScript в браузере пользователя
+- получать доступ к внутренней инфраструктуре
+- записывать файлы на сервере вне разрешённой директории
+- перегружать систему (DoS)
+- получать доступ к данным без аутентификации
+
+⚠️ Приложение **не готово к production-использованию**.
+
+---
+
+## 🎯 Scope
+
+Тестирование охватывало следующие компоненты:
+
+- REST API endpoints:
+ - `/register`
+ - `/recordSession`
+ - `/totalActivity`
+ - `/inactiveUsers`
+ - `/monthlyActivity`
+ - `/userProfile`
+ - `/exportReport`
+ - `/notify`
+- Внутренние сервисы:
+ - `UserAnalyticsService`
+ - `UserStatusService`
+
+---
+
+## 🧱 Asset Inventory
+
+| Актив | Тип | Ценность | Причина |
+|------|-----|----------|--------|
+| Пользовательские данные | Данные | HIGH | Персональные данные |
+| Сессии активности | Данные | HIGH | Аналитика и поведение |
+| Файловая система | Инфраструктура | CRITICAL | Возможен доступ/перезапись |
+| Внутренняя сеть | Инфраструктура | CRITICAL | SSRF атаки |
+
+---
+
+## ⚠️ Threat Modeling (STRIDE)
+
+| Категория | Применимость | Пример |
+|----------|-------------|-------|
+| Spoofing | ✅ | Нет аутентификации |
+| Tampering | ✅ | Изменение данных |
+| Repudiation | ⚠️ | Нет логирования |
+| Information Disclosure | ✅ | Ошибки и SSRF |
+| DoS | ✅ | Нет ограничений |
+| Elevation of Privilege | ⚠️ | Через SSRF |
+
+---
+
+# 🔴 Finding #1 — Reflected XSS
+
+| Поле | Значение |
+|------|----------|
+| Компонент | `/userProfile` |
+| CWE | CWE-79 |
+| CVSS | 6.1 MEDIUM |
+| Статус | Confirmed |
+
+**Описание:**
+Параметр `userName` напрямую вставляется в HTML без экранирования.
+
+**PoC:**
+
+POST /register?userId=evil&userName=
+GET /userProfile?userId=evil
+
+
+**Impact:**
+- выполнение JS
+- кража сессии
+
+**Fix:**
+- HTML escaping
+- CSP header
+
+Шаги воспроизведения:
+
+1. Отправить запрос на регистрацию пользователя с вредоносным payload:
+ POST /register?userId=evil&userName=
+
+2. Запросить профиль пользователя:
+ GET /userProfile?userId=evil
+
+3. Открыть ответ в браузере
+
+Ожидаемый результат:
+HTML должен быть экранирован, скрипт не выполняется
+
+Фактический результат:
+JavaScript выполняется (alert)
+
+---
+
+# 🔴 Finding #2 — Path Traversal
+
+| Поле | Значение |
+|------|----------|
+| Компонент | `/exportReport` |
+| CWE | CWE-22 |
+| CVSS | 8.6 HIGH |
+
+**Описание:**
+Путь к файлу формируется из пользовательского ввода.
+
+**PoC:**
+
+../../../../etc/passwd
+
+
+**Impact:**
+- запись файлов
+- возможный RCE
+
+**Fix:**
+- whitelist файлов
+- normalize path
+
+
+Шаги воспроизведения:
+
+1. Отправить запрос:
+ GET /exportReport?userId=test&filename=../../../../tmp/hacked.txt
+
+2. Проверить статус ответа
+
+Ожидаемый результат:
+Сервер возвращает ошибку (400/403), запись запрещена
+
+Фактический результат:
+Сервер возвращает 200 OK и записывает файл вне разрешённой директории
+
+---
+
+# 🔴 Finding #3 — SSRF
+
+| Поле | Значение |
+|------|----------|
+| Компонент | `/notify` |
+| CWE | CWE-918 |
+| CVSS | 9.1 CRITICAL |
+
+**Описание:**
+Сервер выполняет HTTP-запрос на произвольный URL.
+
+**PoC:**
+
+http://169.254.169.254
+
+
+**Impact:**
+- доступ к metadata
+- обход firewall
+
+**Fix:**
+- блок private IP
+- DNS allowlist
+
+Вариант 1 — localhost
+1. Отправить запрос:
+ POST /notify?userId=test&callbackUrl=http://localhost:7000
+
+2. Наблюдать ответ сервера
+
+Ожидаемый результат:
+Запрос блокируется (400/403)
+
+Фактический результат:
+Сервер выполняет HTTP-запрос к localhost
+
+Вариант 2 — metadata endpoint
+1. Отправить запрос:
+ POST /notify?userId=test&callbackUrl=http://169.254.169.254
+
+2. Проверить ответ
+
+Ожидаемый результат:
+Запрос блокируется
+
+Фактический результат:
+Сервер пытается обратиться к внутреннему IP
+---
+
+# 🔴 Finding #4 — DoS
+
+| Поле | Значение |
+|------|----------|
+| CWE | CWE-400 |
+| CVSS | 7.5 HIGH |
+
+**Описание:**
+- нет rate limiting
+- возможны тяжёлые вычисления (CPU hotspot)
+
+**Impact:**
+- перегрузка CPU
+- зависание сервера
+
+Вариант 1 — большой input
+1. Сформировать userId длиной 10000 символов
+2. Отправить запрос:
+ GET /totalActivity?userId=<очень длинная строка>
+
+Ожидаемый результат:
+Сервер возвращает 400 Bad Request
+
+Фактический результат:
+Сервер обрабатывает запрос без ограничений
+Вариант 2 — flood запросов
+1. Выполнить 50+ запросов подряд:
+ GET /totalActivity?userId=test
+
+2. Проверить ответы
+
+Ожидаемый результат:
+Появляется ограничение (429 Too Many Requests)
+
+Фактический результат:
+Все запросы обрабатываются без ограничений
+
+---
+
+# 🔴 Finding #5 — Broken Validation
+
+| Поле | Значение |
+|------|----------|
+| CWE | CWE-20 |
+
+**Описание:**
+Нет проверки временных интервалов.
+
+**Impact:**
+- некорректная аналитика
+
+Шаги воспроизведения:
+
+1. Отправить запрос:
+ POST /recordSession?userId=test
+ &loginTime=2025-01-01T10:00:00
+ &logoutTime=2025-01-01T09:00:00
+
+2. Проверить статус ответа
+
+Ожидаемый результат:
+Ошибка валидации (400)
+
+Фактический результат:
+Сервер принимает некорректную сессию
+
+---
+
+# 🔴 Finding #6 — Null Handling
+
+| Поле | Значение |
+|------|----------|
+| CWE | CWE-476 |
+
+**Описание:**
+
+getUserSessions(userId).getLast()
+
+
+→ может вызвать NPE
+
+Шаги воспроизведения:
+
+1. Не создавать сессии для пользователя
+2. Выполнить запрос:
+ GET /monthlyActivity?userId=test&month=2025-01
+
+Ожидаемый результат:
+Корректный ответ (пустой результат или 400)
+
+Фактический результат:
+500 Internal Server Error (возможен NullPointerException)
+
+---
+
+# 🔴 Finding #7 — Missing Authentication
+
+| Поле | Значение |
+|------|----------|
+| CWE | CWE-306 |
+| CVSS | 9.8 CRITICAL |
+
+**Описание:**
+Все endpoints доступны без auth.
+
+Шаги воспроизведения:
+
+1. Выполнить запрос без авторизации:
+ GET /totalActivity?userId=test
+
+2. Проверить статус ответа
+
+Ожидаемый результат:
+401 Unauthorized
+
+Фактический результат:
+200 OK — доступ разрешён без проверки
+
+---
+
+# 🔴 Finding #8 — Information Disclosure
+
+| Поле | Значение |
+|------|----------|
+| CWE | CWE-209 |
+
+**Описание:**
+Возвращаются внутренние ошибки.
+
+Шаги воспроизведения:
+
+1. Отправить некорректный запрос:
+ POST /recordSession?userId=test&loginTime=invalid&logoutTime=invalid
+
+2. Проверить тело ответа
+
+Ожидаемый результат:
+Общее сообщение об ошибке (без деталей)
+
+Фактический результат:
+Ответ содержит внутренние детали или текст исключения
+
+---
+
+# 🧪 Security Testing Strategy
+
+Реализованы автоматические тесты:
+
+
+src/test/java/.../pentest/
+
+
+### Подход:
+- тесты проверяют **безопасное поведение**
+- при наличии уязвимости → тест падает
+- после фикса → тест проходит
+
+Шаги воспроизведения:
+
+1. Отправить запрос:
+ POST /register?userId=' OR 1=1 --&userName=test
+
+2. Проверить статус ответа
+
+Ожидаемый результат:
+400 Bad Request (некорректный ввод)
+
+Фактический результат:
+Сервер принимает вредоносный input
+---
+
+# 🛠️ Рекомендации (приоритет)
+
+## 🔴 Critical
+- Добавить authentication
+- Исправить SSRF
+- Исправить Path Traversal
+
+## 🟠 High
+- Валидация input
+- Rate limiting
+
+## 🟡 Medium
+- Обработка ошибок
+- Null safety
+
+---
+
+# 📌 Заключение
+
+Приложение демонстрирует типичные уязвимости:
+
+- отсутствие валидации
+- доверие к пользовательскому вводу
+- отсутствие базовых security-практик
+
+📉 Уровень безопасности: **низкий**
+
+📈 Требуется внедрение:
+- secure coding practices
+- input validation
+- authentication & authorization
diff --git a/README.md b/README.md
index 18eee9a..21b4d8d 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@
+[](https://classroom.github.com/a/NSTTkgmb)
# Лабораторная работа №4 — Анализ и тестирование безопасности веб-приложения
## Цель
diff --git a/pentest.sh b/pentest.sh
new file mode 100644
index 0000000..cf67086
--- /dev/null
+++ b/pentest.sh
@@ -0,0 +1,71 @@
+#!/bin/bash
+
+BASE="http://localhost:7778"
+
+ok() { echo "✅ $1"; }
+fail() { echo "❌ $1"; }
+
+register() {
+ curl -s -X POST "$BASE/register?userId=test&userName=TestUser" > /dev/null
+}
+
+echo "=== SETUP ==="
+register
+
+echo "=== PATH TRAVERSAL ==="
+code=$(curl -s -o /dev/null -w "%{http_code}" \
+"$BASE/exportReport?userId=test&filename=../../../../tmp/hacked.txt")
+
+[[ "$code" != "200" ]] && ok "Path traversal blocked ($code)" || fail "Path traversal NOT blocked"
+
+echo "=== SSRF localhost ==="
+code=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
+"$BASE/notify?userId=test&callbackUrl=http://localhost:7000")
+
+[[ "$code" -ge 400 ]] && ok "SSRF localhost blocked ($code)" || fail "SSRF NOT blocked"
+
+echo "=== SSRF metadata ==="
+code=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
+"$BASE/notify?userId=test&callbackUrl=http://169.254.169.254")
+
+[[ "$code" -ge 400 ]] && ok "SSRF metadata blocked ($code)" || fail "SSRF NOT blocked"
+
+echo "=== LARGE INPUT ==="
+LONG=$(python3 -c "print('A'*10000)")
+code=$(curl -s -o /dev/null -w "%{http_code}" \
+"$BASE/totalActivity?userId=$LONG")
+
+[[ "$code" == "400" ]] && ok "Large input rejected" || fail "No input limit ($code)"
+
+echo "=== RATE LIMIT ==="
+rate_limited=0
+
+for i in {1..50}; do
+ code=$(curl -s -o /dev/null -w "%{http_code}" \
+ "$BASE/totalActivity?userId=test")
+
+ if [[ "$code" == "429" ]]; then
+ rate_limited=1
+ break
+ fi
+done
+
+[[ "$rate_limited" == "1" ]] && ok "Rate limit works" || fail "NO rate limit"
+
+echo "=== INVALID SESSION ==="
+code=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
+"$BASE/recordSession?userId=test&loginTime=2025-01-01T10:00:00&logoutTime=2025-01-01T09:00:00")
+
+[[ "$code" != "200" ]] && ok "Invalid session rejected" || fail "Bad session accepted"
+
+echo "=== NO AUTH ==="
+code=$(curl -s -o /dev/null -w "%{http_code}" \
+"$BASE/totalActivity?userId=test")
+
+[[ "$code" == "401" ]] && ok "Auth required" || fail "Auth missing"
+
+echo "=== SQLi TEST ==="
+code=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
+"$BASE/register?userId=%27%20OR%201%3D1%20--&userName=test")
+
+[[ "$code" == "400" ]] && ok "Input filtered" || fail "SQLi not blocked"
\ No newline at end of file
diff --git a/src/test/java/ru/itmo/testing/lab4/pentest/SecurityPentestTest.java b/src/test/java/ru/itmo/testing/lab4/pentest/SecurityPentestTest.java
new file mode 100644
index 0000000..96529f8
--- /dev/null
+++ b/src/test/java/ru/itmo/testing/lab4/pentest/SecurityPentestTest.java
@@ -0,0 +1,214 @@
+package ru.itmo.testing.lab4.pentest;
+
+import io.javalin.Javalin;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+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;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+class SecurityPentestTest {
+
+ private static final int TEST_PORT = 7778;
+ 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();
+ }
+
+ // =========================================================================
+ // HELPER
+ // =========================================================================
+
+ 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());
+ }
+
+ @BeforeEach
+ void setupUser() throws Exception {
+ send("POST", "/register?userId=test&userName=TestUser");
+ }
+
+ // =========================================================================
+ // PATH TRAVERSAL — /exportReport
+ // =========================================================================
+
+ @Test
+ @DisplayName("[SECURITY] Path Traversal должен быть заблокирован")
+ void pathTraversalShouldBeBlocked() throws Exception {
+ HttpResponse response = send("GET",
+ "/exportReport?userId=test&filename=../../../../tmp/hacked.txt");
+
+ assertNotEquals(200, response.statusCode(),
+ "Path Traversal не заблокирован!");
+ }
+
+
+ // =========================================================================
+ // SSRF — /notify
+ // =========================================================================
+
+ @Test
+ @DisplayName("[SECURITY] SSRF на localhost должен блокироваться")
+ void ssrfLocalhostShouldBeBlocked() throws Exception {
+ HttpResponse response = send("POST",
+ "/notify?userId=test&callbackUrl=http://localhost:7000");
+
+ assertTrue(response.statusCode() >= 400,
+ "SSRF на localhost разрешён!");
+ }
+
+ @Test
+ @DisplayName("[SECURITY] SSRF на metadata IP должен блокироваться")
+ void ssrfMetadataShouldBeBlocked() throws Exception {
+ HttpResponse response = send("POST",
+ "/notify?userId=test&callbackUrl=http://169.254.169.254");
+
+ assertTrue(response.statusCode() >= 400,
+ "SSRF на внутренние IP разрешён!");
+ }
+
+ // =========================================================================
+ // DoS — рекурсия / heavy input
+ // =========================================================================
+
+ @Test
+ @DisplayName("[SECURITY] Слишком длинный input должен отклоняться")
+ void largeInputShouldBeRejected() throws Exception {
+ String largeUserId = "A".repeat(10000);
+
+ HttpResponse response = send("GET",
+ "/totalActivity?userId=" + largeUserId);
+
+ assertEquals(400, response.statusCode(),
+ "Нет ограничения на размер входных данных!");
+ }
+
+ @Test
+ @DisplayName("[SECURITY] Flood запросов должен ограничиваться")
+ void requestFloodShouldBeLimited() throws Exception {
+ boolean rateLimited = false;
+
+ for (int i = 0; i < 50; i++) {
+ HttpResponse response = send("GET",
+ "/totalActivity?userId=test");
+
+ if (response.statusCode() == 429) {
+ rateLimited = true;
+ break;
+ }
+ }
+
+ assertTrue(rateLimited,
+ "Отсутствует rate limiting!");
+ }
+
+ // =========================================================================
+ // BUSINESS LOGIC — invalid session
+ // =========================================================================
+
+ @Test
+ @DisplayName("[SECURITY] logoutTime < loginTime должно отклоняться")
+ void invalidSessionTimeShouldFail() throws Exception {
+ HttpResponse response = send("POST",
+ "/recordSession?userId=test"
+ + "&loginTime=2025-01-01T10:00:00"
+ + "&logoutTime=2025-01-01T09:00:00");
+
+ assertNotEquals(200, response.statusCode(),
+ "Некорректная сессия принимается!");
+ }
+
+ // =========================================================================
+ // NULL POINTER / ERROR HANDLING
+ // =========================================================================
+
+ @Test
+ @DisplayName("[SECURITY] Отсутствие сессий не должно ломать сервер")
+ void nullPointerShouldBeHandled() throws Exception {
+ HttpResponse response = send("GET",
+ "/monthlyActivity?userId=test&month=2025-01");
+
+ assertNotEquals(500, response.statusCode(),
+ "NullPointerException приводит к 500!");
+ }
+
+ // =========================================================================
+ // INFORMATION DISCLOSURE
+ // =========================================================================
+
+ @Test
+ @DisplayName("[SECURITY] Ошибки не должны раскрывать детали")
+ void errorMessagesShouldBeSanitized() throws Exception {
+ HttpResponse response = send("POST",
+ "/recordSession?userId=test&loginTime=invalid&logoutTime=invalid");
+
+ assertFalse(response.body().contains("Exception"),
+ "Раскрываются внутренние детали!");
+ }
+
+ // =========================================================================
+ // MISSING AUTH
+ // =========================================================================
+
+ @Test
+ @DisplayName("[SECURITY] Доступ без аутентификации должен быть запрещён")
+ void noAuthShouldBeRejected() throws Exception {
+ HttpResponse response = send("GET",
+ "/totalActivity?userId=test");
+
+ assertEquals(401, response.statusCode(),
+ "Нет проверки аутентификации!");
+ }
+
+ // =========================================================================
+ // INPUT VALIDATION
+ // =========================================================================
+
+ @Test
+ @DisplayName("[SECURITY] Спецсимволы должны фильтроваться")
+ void specialCharsShouldBeRejected() throws Exception {
+ String payload = "' OR 1=1 --";
+
+ HttpResponse response = send("POST",
+ "/register?userId=" + enc(payload) + "&userName=test");
+
+ assertEquals(400, response.statusCode(),
+ "Нет валидации входных данных!");
+ }
+}