From 98c6072e84cd73969e0a69fc5e7e0455d66a1440 Mon Sep 17 00:00:00 2001
From: "github-classroom[bot]"
<66690702+github-classroom[bot]@users.noreply.github.com>
Date: Sun, 12 Apr 2026 11:03:16 +0000
Subject: [PATCH 1/9] Setting up GitHub Classroom Feedback
From 83d36334c93cb5b07d3edee74cab37a61859915d Mon Sep 17 00:00:00 2001
From: "github-classroom[bot]"
<66690702+github-classroom[bot]@users.noreply.github.com>
Date: Sun, 12 Apr 2026 11:03:18 +0000
Subject: [PATCH 2/9] add deadline
---
README.md | 1 +
1 file changed, 1 insertion(+)
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 — Анализ и тестирование безопасности веб-приложения
## Цель
From b6f2066b28700f75d53d9eac5be4b68cd5910664 Mon Sep 17 00:00:00 2001
From: DenisStepanidenko <110686828+DenisStepanidenko@users.noreply.github.com>
Date: Sun, 12 Apr 2026 16:29:46 +0300
Subject: [PATCH 3/9] Update README.md
---
README.md | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index 21b4d8d..af2a85f 100644
--- a/README.md
+++ b/README.md
@@ -43,12 +43,14 @@
| Актив | Тип | Ценность | Примечание |
|-------|-----|----------|------------|
-| Данные пользователей (userId, userName) | Данные | ? | |
-| Данные о сессиях (время входа/выхода) | Данные | ? | |
-| Файловая система сервера | Инфраструктура | ? | |
-| Внутренняя сеть / метаданные окружения | Инфраструктура | ? | |
+| Данные пользователей (userId, userName) | Данные | Высокая | В нашей системе, зная userId можно смотреть чужую информацию, так как никакой проверки прав не реализовано|
+| Данные о сессиях (время входа/выхода) | Данные | Низкая | Зная только время входа и выхода, никаких данных других получить нельзя |
+| Файловая система сервера | Инфраструктура | Высокая | Path Traversal. Возможны чтения системных файлов (/etc/passwd), чтение исходного кода (конфигурация БД, api ключи), запись файла в любое место (remote code execution, загрузка веб-шелла)|
+| Внутренняя сеть / метаданные окружения | Инфраструктура | Высокая | Возможны Server-Side Request Forgery, подделка запроса от имени сервера для, например, кражи api ключей|
> **Вопрос для размышления:** какие из активов наиболее критичны и почему?
+> Наиболее критичны Внутренняя сеть/метаданные окружения и файловая система сервера.
+> Внутренняя сеть и метаданные облака — самый критичный актив, потому что через SSRF-уязвимость злоумышленник получает доступ не к одному серверу, а ко всей облачной инфраструктуре компании. Файловая система сервера критична, потому что через Path Traversal атакующий может записать веб-шелл в любую директорию и получить удаленное выполнение команд на сервере. Это дает полный контроль над самим хостом.
---
From 787df876dd1db2ca496188a231df95fe81ad36e4 Mon Sep 17 00:00:00 2001
From: DenisStepanidenko <110686828+DenisStepanidenko@users.noreply.github.com>
Date: Sun, 12 Apr 2026 16:31:01 +0300
Subject: [PATCH 4/9] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index af2a85f..a5e0dfa 100644
--- a/README.md
+++ b/README.md
@@ -44,7 +44,7 @@
| Актив | Тип | Ценность | Примечание |
|-------|-----|----------|------------|
| Данные пользователей (userId, userName) | Данные | Высокая | В нашей системе, зная userId можно смотреть чужую информацию, так как никакой проверки прав не реализовано|
-| Данные о сессиях (время входа/выхода) | Данные | Низкая | Зная только время входа и выхода, никаких данных других получить нельзя |
+| Данные о сессиях (время входа/выхода) | Данные | Низкая | Зная только время входа и выхода, никаких данных других получить нельзя. Однако отсутствие авторизации позволяет подделывать сессии других пользователей, искажая аналитику. |
| Файловая система сервера | Инфраструктура | Высокая | Path Traversal. Возможны чтения системных файлов (/etc/passwd), чтение исходного кода (конфигурация БД, api ключи), запись файла в любое место (remote code execution, загрузка веб-шелла)|
| Внутренняя сеть / метаданные окружения | Инфраструктура | Высокая | Возможны Server-Side Request Forgery, подделка запроса от имени сервера для, например, кражи api ключей|
From d48e40cbe4677b0c84acc8dd4d69fc40e50ee7b5 Mon Sep 17 00:00:00 2001
From: DenisStepanidenko <110686828+DenisStepanidenko@users.noreply.github.com>
Date: Sun, 12 Apr 2026 22:24:46 +0300
Subject: [PATCH 5/9] Update README.md
---
README.md | 15 +++++++++------
1 file changed, 9 insertions(+), 6 deletions(-)
diff --git a/README.md b/README.md
index a5e0dfa..410a751 100644
--- a/README.md
+++ b/README.md
@@ -60,12 +60,12 @@
| Категория угрозы | Расшифровка | Применимо к этому приложению? |
|------------------|------------------------|-------------------------------|
-| **S**poofing | Подмена идентификации | ? |
-| **T**ampering | Модификация данных | ? |
-| **R**epudiation | Отказ от авторства | ? |
-| **I**nformation Disclosure | Утечка данных | ? |
-| **D**enial of Service | Отказ в обслуживании | ? |
-| **E**levation of Privilege | Повышение привилегий | ? |
+| **S**poofing | Подмена идентификации | Да. Источник угрозы - внешний злоумышленник. Все эндпоинты, принимающие userId как параметр. Потенциальный ущерб - доступ к чужим данным и выполненией действий от имени другого пользователя. |
+| **T**ampering | Модификация данных | Да. Источник угроы - внешний злоумышленник. Эндпоинты /recordSession, /exportData. Искажение аналитики, подделка отчётов, запись произвольных файлов на сервер. |
+| **R**epudiation | Отказ от авторства | Да. Источник угрозы - внешний или внутренний пользователь. Поверхность атака - эндпоинты, где нет логирования. Невозможность доказать факт совершения действия, расследовать инцидент. |
+| **I**nformation Disclosure | Утечка данных | Да. Источник угрозы - внешний злоумышленник. Поверхность атаки - эндпоинты /userProfile, /totalActivity |
+| **D**enial of Service | Отказ в обслуживании | Да. Внешнией злоумешленник. Поверхность атаки - /register, /recordSession. В приложении нет rate limiting, поэтому можно сделать тысячи запросов в секунду, зарегистрировать миллионы пользователей и сессий, что приведёт в outOfMemoryException. |
+| **E**levation of Privilege | Повышение привилегий | Да. Внешний злоумышленник. /exportReport, /notify. Получение прав на выполнение на сервере, доступ к облачной инфраструктуре. |
Для каждой применимой угрозы укажите:
- **Источник угрозы** (кто/что может её реализовать)
@@ -102,6 +102,9 @@ curl "http://localhost:7000/userProfile?userId=evil"
> Данный пример — лишь отправная точка. Исследуйте **все** эндпоинты самостоятельно.
+
+
+
---
### Этап 4 — Статический анализ с Semgrep
From 724c5b4711dda59b114a3db94e4136f908456263 Mon Sep 17 00:00:00 2001
From: DenisStepanidenko <110686828+DenisStepanidenko@users.noreply.github.com>
Date: Mon, 13 Apr 2026 21:12:40 +0300
Subject: [PATCH 6/9] Update README.md
---
README.md | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/README.md b/README.md
index 410a751..96e94be 100644
--- a/README.md
+++ b/README.md
@@ -102,6 +102,11 @@ curl "http://localhost:7000/userProfile?userId=evil"
> Данный пример — лишь отправная точка. Исследуйте **все** эндпоинты самостоятельно.
+Коллекция ручных запросов в Postman:
+
+
+
+Пример XSS alert:
From 9741b899165c11e65ade86ba473460d95f9c6100 Mon Sep 17 00:00:00 2001
From: DenisStepanidenko <110686828+DenisStepanidenko@users.noreply.github.com>
Date: Tue, 14 Apr 2026 00:22:29 +0300
Subject: [PATCH 7/9] Update README.md
---
README.md | 563 ++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 544 insertions(+), 19 deletions(-)
diff --git a/README.md b/README.md
index 96e94be..7b2fcca 100644
--- a/README.md
+++ b/README.md
@@ -191,6 +191,130 @@ semgrep --config "p/java" --sarif -o semgrep-report.sarif src/
> **Важно:** Semgrep — вспомогательный инструмент. Статический анализ не заменяет ручное тестирование. Некоторые уязвимости он найдёт, некоторые — нет.
+Результат работы semgrep
+
+```
+✅ Scan completed successfully.
+ • Findings: 0 (0 blocking)
+ • Rules run: 113
+ • Targets scanned: 6
+ • Parsed lines: ~100.0%
+ • Scan skipped:
+ ◦ Files matching .semgrepignore patterns: 1
+ • Scan was limited to files tracked by git
+ • For a detailed list of skipped files and lines, run semgrep with the --verbose flag
+Ran 113 rules on 6 files: 0 findings
+```
+Был выполнен запуск Semgrep с официальным репозиторием правил для Java (113 правил). Найдено 0 findings, так как правила ориентированы на Spring/Jakarta EE и не покрывают паттерны фреймворка Javalin.
+Мы создали правила специально под Javalin
+
+Файл rules.yaml
+
+```java
+rules:
+ - id: javalin-xss-in-userprofile
+ patterns:
+ - pattern: |
+ ctx.contentType("text/html").result(...)
+ - pattern: |
+ $HTML + $USERINPUT
+ message: |
+ Potential Reflected XSS (CWE-79).
+ User-controlled data (userName) is concatenated directly into HTML response without escaping.
+ Use HTML encoding (e.g., StringEscapeUtils.escapeHtml4) or a template engine.
+ languages:
+ - java
+ severity: ERROR
+ metadata:
+ cwe: "CWE-79"
+ owasp: "A03:2021 - Injection"
+ confidence: HIGH
+
+ - id: javalin-path-traversal-in-export
+ pattern: |
+ new File($DIR + $FILENAME)
+ message: |
+ Potential Path Traversal (CWE-22).
+ User-controlled filename is concatenated with base directory without validation.
+ Attacker can use "../" to write files outside /tmp/reports/.
+ Use Paths.get(baseDir).resolve(filename).normalize() and verify the result starts with baseDir.
+ languages:
+ - java
+ severity: ERROR
+ metadata:
+ cwe: "CWE-22"
+ owasp: "A01:2021 - Broken Access Control"
+ confidence: HIGH
+
+ - id: javalin-ssrf-in-notify
+ patterns:
+ - pattern: |
+ URL $URL = new URL($USERINPUT);
+ ...
+ $URL.openConnection();
+ message: |
+ Potential Server-Side Request Forgery (CWE-918).
+ User-controlled URL is used to make server-side HTTP requests without validation.
+ Attacker can access internal services (localhost, 169.254.169.254, internal IPs).
+ Validate URL against allowlist and block private/internal IP ranges.
+ languages:
+ - java
+ severity: ERROR
+ metadata:
+ cwe: "CWE-918"
+ owasp: "A10:2021 - Server-Side Request Forgery"
+ confidence: HIGH
+
+ - id: javalin-info-disclosure-in-errors
+ pattern-either:
+ - pattern: |
+ ctx.status(400).result("Invalid data: " + e.getMessage())
+ - pattern: |
+ ctx.status(400).result(e.getMessage())
+ message: |
+ Potential Information Disclosure (CWE-209).
+ Exception messages are returned directly to the client, revealing internal logic.
+ Return generic error messages and log detailed errors server-side.
+ languages:
+ - java
+ severity: WARNING
+ metadata:
+ cwe: "CWE-209"
+ confidence: MEDIUM
+
+```
+
+Результаты работы, найдено 4 уязвимости по нашим правилам.
+
+```java
+ src/main/java/ru/itmo/testing/lab4/controller/UserAnalyticsController.java
+ ❯❱ javalin-info-disclosure-in-errors
+ ❰❰ Blocking ❱❱
+ Potential Information Disclosure (CWE-209). Exception messages are returned directly to the client,
+ revealing internal logic. Return generic error messages and log detailed errors server-side.
+
+ 74┆ ctx.status(400).result("Invalid data: " + e.getMessage());
+ ⋮┆----------------------------------------
+ 115┆ ctx.status(400).result("Invalid data: " + e.getMessage());
+
+ ❯❯❱ javalin-path-traversal-in-export
+ ❰❰ Blocking ❱❱
+ Potential Path Traversal (CWE-22). User-controlled filename is concatenated with base directory
+ without validation. Attacker can use "../" to write files outside /tmp/reports/. Use
+ Paths.get(baseDir).resolve(filename).normalize() and verify the result starts with baseDir.
+
+ 154┆ File reportFile = new File(REPORTS_BASE_DIR + filename);
+
+ ❯❯❱ javalin-ssrf-in-notify
+ ❰❰ Blocking ❱❱
+ Potential Server-Side Request Forgery (CWE-918). User-controlled URL is used to make server-side
+ HTTP requests without validation. Attacker can access internal services (localhost, 169.254.169.254,
+ internal IPs). Validate URL against allowlist and block private/internal IP ranges.
+
+ 180┆ URL url = new URL(callbackUrl);
+ 181┆ URLConnection connection = url.openConnection();
+```
+
---
### Этап 5 — Оформление отчёта
@@ -199,44 +323,445 @@ semgrep --config "p/java" --sarif -o semgrep-report.sarif src/
---
-#### 🔴 Finding #N — [Название уязвимости]
+#### 🔴 Finding #1 — Reflected Cross-Site Scripting (XSS)
+
+| Поле | Значение |
+|------|----------|
+| **Компонент** | UserAnalyticsController.java, эндпоинт GET /userProfile |
+| **Тип** | 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 из хранилища напрямую в разметку без экранирования. Злоумышленник может зарегистрировать пользователя с именем, содержащим JavaScript-код. Когда жертва откроет профиль, скрипт выполнится в её браузере.
+
+**Шаги воспроизведения:**
+```
+1. POST /register?userId=xss_test&userName=
+2. GET /userProfile?userId=xss_test
+3. Ожидаемый результат: тег отображается как текст (<script>...)
+ Фактический результат: тег вставлен в HTML как есть, браузер выполняет скрипт
+```
+
+**Влияние:**
+> Атакующий может украсть cookies, токены из localStorage, перенаправить жертву на фишинговый сайт, выполнить действия от имени жертвы.
+
+**Рекомендации по исправлению:**
+> Экранировать HTML-спецсимволы перед вставкой в разметку. Например, использовать
+
+**Security Test Case:**
+```java
+XssPentestTest.java
+```
+
+---
+
+#### 🔴 Finding #2 — Path Traversal
+
+| Поле | Значение |
+|------|----------|
+| **Компонент** | UserAnalyticsController.java, эндпоинт GET /exportReport |
+| **Тип** | Path Traversal |
+| **CWE** | CWE-22 — Improper Limitation of a Pathname to a Restricted Directory |
+| **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 и конкатенирует его с REPORTS_BASE_DIR без валидации. Атакующий может использовать ../ для выхода за пределы /tmp/reports/ и записи файла в произвольную директорию, что ведет к RCE (Remote Code Execution) через загрузку веб-шелла (командная оболочка для удалённого управления веб-сервером).
+
+**Шаги воспроизведения:**
+```
+1. POST /register?userId=test&userName=Test
+2. GET /exportReport?userId=test&filename=../../../tmp/hack.txt
+3. Ожидаемый результат: файл создается в /tmp/reports/ или запрос отклоняется
+ Фактический результат: файл создается в /tmp/hack.txt
+```
+
+**Влияние:**
+> Атакующий может записать веб-шелл (командная оболочка для удалённого управления веб-сервером) в директорию веб-сервера и получить удаленное выполнение команд (remote code execution), что дает контроль над сервером.
+
+**Рекомендации по исправлению:**
+> Использовать Paths.get(REPORTS_BASE_DIR).resolve(filename).normalize() и проверять, что итоговый путь начинается с REPORTS_BASE_DIR
+
+**Security Test Case:**
+```java
+class PathTraversalTest {
+
+ private static final int TEST_PORT = 7000;
+ private static final String BASE_URL = "http://localhost:" + TEST_PORT;
+ private static final Path EVIL_FILE = Paths.get("/tmp/hack.txt");
+
+ 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();
+ }
+
+ @BeforeEach
+ void cleanup() throws Exception {
+ Files.deleteIfExists(EVIL_FILE);
+ }
+
+ @Test
+ @DisplayName("[SECURITY] Path Traversal allows writing file outside /tmp/reports/")
+ void pathTraversalWritesOutsideReportsDir() throws Exception {
+ // Arrange: регистрируем пользователя
+ HttpRequest registerReq = HttpRequest.newBuilder()
+ .uri(URI.create(BASE_URL + "/register?userId=test&userName=Test"))
+ .POST(HttpRequest.BodyPublishers.noBody())
+ .build();
+ http.send(registerReq, HttpResponse.BodyHandlers.ofString());
+
+ // Act: пытаемся записать файл в /tmp/ через Path Traversal
+ String payload = "../hack.txt";
+ HttpRequest exportReq = HttpRequest.newBuilder()
+ .uri(URI.create(BASE_URL + "/exportReport?userId=test&filename=" + payload))
+ .GET()
+ .build();
+ HttpResponse response = http.send(exportReq, HttpResponse.BodyHandlers.ofString());
+
+ // Assert: уязвимость подтверждена — файл создался за пределами /tmp/reports/
+ assertTrue(Files.exists(EVIL_FILE),
+ "FAIL: File was created outside /tmp/reports/. Path Traversal confirmed.");
+ }
+
+}
+```
+
+---
+
+#### 🔴 Finding #3 — Server-Side Request Forgery (SSRF)
+
+| Поле | Значение |
+|------|----------|
+| **Компонент** | UserAnalyticsController.java, эндпоинт POST /notify |
+| **Тип** | Server-Side Request Forgery (SSRF) |
+| **CWE** | CWE-918 — Server-Side Request Forgery (SSRF) |
+| **CVSS v3.1** | 7.5 HIGH (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N) |
+| **Статус** | Confirmed |
+
+**Описание:**
+> Эндпоинт /notify принимает параметр callbackUrl и делает HTTP-запрос по этому адресу без валидации. Сервер выступает как прокси, позволяя атакующему сканировать внутреннюю сеть, получать доступ к облачным метаданным (169.254.169.254) и атаковать внутренние сервисы.
+
+**Шаги воспроизведения:**
+```
+1. POST /register?userId=test&userName=Test
+2. POST /notify?userId=test&callbackUrl=http://127.0.0.1:7000/userProfile?userId=test
+3. Ожидаемый результат: запрос отклонен (403/400)
+ Фактический результат: сервер выполняет запрос и возвращает HTML своего же эндпоинта
+```
+
+**Влияние:**
+> Атакующий может украсть IAM-ключи из облачных метаданных, получить доступ к внутренним базам данных, отсканировать локальные порты, обойти файрвол.
+
+**Рекомендации по исправлению:**
+> Валидировать URL
+
+**Security Test Case:**
+```java
+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.URLEncoder;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class SSRTTest {
+
+ private static final int TEST_PORT = 7779;
+ 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("[SECURITY] SSRF allows request to localhost")
+ void ssrfAllowsLocalhostRequest() throws Exception {
+ // Arrange: регистрируем пользователя
+ HttpRequest registerReq = HttpRequest.newBuilder()
+ .uri(URI.create(BASE_URL + "/register?userId=test&userName=Test"))
+ .POST(HttpRequest.BodyPublishers.noBody())
+ .build();
+ http.send(registerReq, HttpResponse.BodyHandlers.ofString());
+
+ // Act: пытаемся сделать запрос к localhost (сами к себе)
+ String internalUrl = "http://127.0.0.1:" + TEST_PORT + "/userProfile?userId=test";
+ String encodedUrl = URLEncoder.encode(internalUrl, StandardCharsets.UTF_8);
+ HttpRequest notifyReq = HttpRequest.newBuilder()
+ .uri(URI.create(BASE_URL + "/notify?userId=test&callbackUrl=" + encodedUrl))
+ .POST(HttpRequest.BodyPublishers.noBody())
+ .build();
+ HttpResponse response = http.send(notifyReq, HttpResponse.BodyHandlers.ofString());
+
+ // Assert: уязвимость подтверждена — сервер сделал запрос к localhost
+ assertTrue(response.body().contains("Profile: Test"),
+ "FAIL: SSRF confirmed — server made request to localhost and returned internal data.");
+ }
+
+}
+```
+
+---
+
+
+#### 🔴 Finding #4 — Server-Side Request Forgery (SSRF)
+
+| Поле | Значение |
+|------|----------|
+| **Компонент** | UserAnalyticsController.java, эндпоинты /recordSession, /monthlyActivity |
+| **Тип** | Information Disclosure through Error Messages |
+| **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 |
+
+**Описание:**
+> При возникновении ошибок (неверный формат даты, отсутствие сессий) приложение возвращает клиенту полный текст исключения: "Invalid data: " + e.getMessage(). Это раскрывает внутреннюю логику, используемые библиотеки и структуру системы.
+
+**Шаги воспроизведения:**
+```
+1. GET /monthlyActivity?userId=ghost&month=2026/01
+2. Ожидаемый результат: "Invalid request" или "Bad request"
+ Фактический результат: "Invalid data: Text '2026/01' could not be parsed at index 4"
+```
+
+**Влияние:**
+> Атакующий получает информацию о внутреннем устройстве приложения (Java, парсинг дат), что помогает в разведке и построении более сложных атак.
+
+**Рекомендации по исправлению:**
+> Возвращать обобщенные сообщения об ошибках: "Invalid request parameters". Детали ошибки логировать на сервере, но не отправлять клиенту.
+
+**Security Test Case:**
+```java
+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.*;
+
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+class InfoDisclosureTest {
+
+ private static final int TEST_PORT = 7780;
+ 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("[SECURITY] Error messages disclose internal parsing details")
+ void errorMessagesDiscloseInternalDetails() throws Exception {
+ // Act: отправляем невалидный формат месяца
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(BASE_URL + "/monthlyActivity?userId=test&month=2026/01"))
+ .GET()
+ .build();
+ HttpResponse response = http.send(request, HttpResponse.BodyHandlers.ofString());
+
+ // Assert: уязвимость подтверждена — ответ содержит детали парсинга
+ assertEquals(400, response.statusCode());
+ assertTrue(response.body().contains("could not be parsed"),
+ "FAIL: Error message discloses internal parsing details.");
+ assertTrue(response.body().contains("Text '2026/01'"),
+ "FAIL: User input reflected in error message with parsing context.");
+ }
+
+ @Test
+ @Order(2)
+ @DisplayName("[SECURITY] Error message discloses business logic for non-existent user")
+ void errorMessageDisclosesBusinessLogic() throws Exception {
+ // Act: запрашиваем несуществующего пользователя
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(BASE_URL + "/monthlyActivity?userId=nonexistent&month=2026-01"))
+ .GET()
+ .build();
+ HttpResponse response = http.send(request, HttpResponse.BodyHandlers.ofString());
+
+ // Assert: уязвимость подтверждена — раскрывается информация о сессиях
+ assertEquals(400, response.statusCode());
+ assertTrue(response.body().contains("No sessions found for user"),
+ "FAIL: Error message discloses that user exists but has no sessions.");
+ }
+
+}
+```
+
+---
+
+#### 🔴 Finding #5 — Missing Authentication / Broken Access Control
| Поле | Значение |
|------|----------|
-| **Компонент** | Эндпоинт или класс, где обнаружена уязвимость |
-| **Тип** | Краткое название (например: Reflected XSS) |
-| **CWE** | [CWE-XXX](https://cwe.mitre.org/data/definitions/XXX.html) — название |
-| **CVSS v3.1** | Числовой балл (0.0–10.0) и вектор, например: `7.5 HIGH (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)` |
-| **Статус** | Confirmed / Suspected / False Positive |
+| **Компонент** | Все эндпоинты приложения |
+| **Тип** | Broken Access Control / Missing Authentication |
+| **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:H/A:N) |
+| **Статус** | Confirmed |
**Описание:**
-> Что происходит и почему это проблема.
+> Приложение не реализует механизм аутентификации. Любой запрос с корректным userId обрабатывается без проверки, что инициатор имеет право действовать от имени этого пользователя. Знание userId дает полный доступ к данным и действиям пользователя.
**Шаги воспроизведения:**
```
-1. ...
-2. ...
-3. Ожидаемый результат: ...
- Фактический результат: ...
+1. POST /register?userId=alice&userName=Alice
+2. POST /register?userId=bob&userName=Bob (злоумышленник)
+3. GET /totalActivity?userId=alice (злоумышленник запрашивает данные Alice)
+4. Ожидаемый результат: 403 Forbidden
+ Фактический результат: возвращаются данные Alice
```
**Влияние:**
-> Что может сделать атакующий, если воспользуется уязвимостью.
+> Злоумышленник может получить доступ к данным любого пользователя, подделывать сессии, искажать аналитику, дискредитировать пользователей. Возможен массовый сбор данных через перебор userId.
**Рекомендации по исправлению:**
-> Конкретные меры: какой метод/библиотеку использовать, какую проверку добавить.
+> Внедрить аутентификацию (JWT, OAuth2, Session-based). Хранить userId в контексте сессии, а не передавать в query-параметрах. Все эндпоинты должны использовать userId из аутентифицированной сессии.
**Security Test Case:**
```java
-@Test
-@DisplayName("[SECURITY] ...")
-void testName() {
- // Arrange
- // Act
- // Assert — проверить, что уязвимость закрыта
+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.*;
+
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+class MissingAuthPentestTest {
+
+ private static final int TEST_PORT = 7781;
+ 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("[SECURITY] Any user can access another user's data")
+ void anyUserCanAccessAnotherUsersData() throws Exception {
+ // Arrange: создаем двух пользователей
+ http.send(HttpRequest.newBuilder()
+ .uri(URI.create(BASE_URL + "/register?userId=alice&userName=Alice"))
+ .POST(HttpRequest.BodyPublishers.noBody())
+ .build(), HttpResponse.BodyHandlers.ofString());
+
+ http.send(HttpRequest.newBuilder()
+ .uri(URI.create(BASE_URL + "/register?userId=bob&userName=Bob"))
+ .POST(HttpRequest.BodyPublishers.noBody())
+ .build(), HttpResponse.BodyHandlers.ofString());
+
+ // Добавляем сессию Alice
+ http.send(HttpRequest.newBuilder()
+ .uri(URI.create(BASE_URL + "/recordSession?userId=alice&loginTime=2026-01-15T10:00:00&logoutTime=2026-01-15T11:00:00"))
+ .POST(HttpRequest.BodyPublishers.noBody())
+ .build(), HttpResponse.BodyHandlers.ofString());
+
+ // Act: Bob (злоумышленник) запрашивает данные Alice
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(BASE_URL + "/totalActivity?userId=alice"))
+ .GET()
+ .build();
+ HttpResponse response = http.send(request, HttpResponse.BodyHandlers.ofString());
+
+ // Assert: уязвимость подтверждена — Bob получил данные Alice
+ assertEquals(200, response.statusCode());
+ assertTrue(response.body().contains("60 minutes") || response.body().contains("Total activity:"),
+ "FAIL: Missing authentication allows access to another user's data.");
+ }
+
+ @Test
+ @Order(2)
+ @DisplayName("[SECURITY] Any user can add sessions for another user")
+ void anyUserCanAddSessionsForAnotherUser() throws Exception {
+ // Act: Bob добавляет сессию для Alice
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(BASE_URL + "/recordSession?userId=alice&loginTime=2026-01-15T12:00:00&logoutTime=2026-01-15T13:00:00"))
+ .POST(HttpRequest.BodyPublishers.noBody())
+ .build();
+ HttpResponse response = http.send(request, HttpResponse.BodyHandlers.ofString());
+
+ // Assert: уязвимость подтверждена — сессия добавлена
+ assertEquals(200, response.statusCode());
+ assertTrue(response.body().contains("Session recorded"),
+ "FAIL: Missing authentication allows modifying another user's data.");
+ }
+
}
```
+
+
+
---
### Пример оформленного finding
From 4a76c4e8f37775d6604888d7a4926d43600b1110 Mon Sep 17 00:00:00 2001
From: DenisStepanidenko
Date: Tue, 14 Apr 2026 00:23:22 +0300
Subject: [PATCH 8/9] implement lab_4
---
.idea/.name | 1 +
.idea/gradle.xml | 2 +-
.idea/vcs.xml | 2 +-
build.gradle | 1 +
rules.yaml | 70 +++++++++++++++
semgrep-report.sarif | 1 +
semgrep1-report.sarif | 1 +
.../lab4/pentest/InfoDisclosureTest.java | 71 +++++++++++++++
.../lab4/pentest/MissingAuthPentestTest.java | 86 +++++++++++++++++++
.../lab4/pentest/PathTraversalTest.java | 67 +++++++++++++++
.../itmo/testing/lab4/pentest/SSRTTest.java | 64 ++++++++++++++
.../testing/lab4/pentest/XssPentestTest.java | 1 +
12 files changed, 365 insertions(+), 2 deletions(-)
create mode 100644 .idea/.name
create mode 100644 rules.yaml
create mode 100644 semgrep-report.sarif
create mode 100644 semgrep1-report.sarif
create mode 100644 src/test/java/ru/itmo/testing/lab4/pentest/InfoDisclosureTest.java
create mode 100644 src/test/java/ru/itmo/testing/lab4/pentest/MissingAuthPentestTest.java
create mode 100644 src/test/java/ru/itmo/testing/lab4/pentest/PathTraversalTest.java
create mode 100644 src/test/java/ru/itmo/testing/lab4/pentest/SSRTTest.java
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/gradle.xml b/.idea/gradle.xml
index 9a03a98..ce1c62c 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/build.gradle b/build.gradle
index fb0a8f7..f1f4833 100644
--- a/build.gradle
+++ b/build.gradle
@@ -16,6 +16,7 @@ dependencies {
testImplementation platform('org.junit:junit-bom:6.0.0')
testImplementation 'org.junit.jupiter:junit-jupiter'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+ implementation("com.fasterxml.jackson.core:jackson-databind:2.17.2")
}
test {
diff --git a/rules.yaml b/rules.yaml
new file mode 100644
index 0000000..f8b04a0
--- /dev/null
+++ b/rules.yaml
@@ -0,0 +1,70 @@
+rules:
+ - id: javalin-xss-in-userprofile
+ patterns:
+ - pattern: |
+ ctx.contentType("text/html").result(...)
+ - pattern: |
+ $HTML + $USERINPUT
+ message: |
+ Potential Reflected XSS (CWE-79).
+ User-controlled data (userName) is concatenated directly into HTML response without escaping.
+ Use HTML encoding (e.g., StringEscapeUtils.escapeHtml4) or a template engine.
+ languages:
+ - java
+ severity: ERROR
+ metadata:
+ cwe: "CWE-79"
+ owasp: "A03:2021 - Injection"
+ confidence: HIGH
+
+ - id: javalin-path-traversal-in-export
+ pattern: |
+ new File($DIR + $FILENAME)
+ message: |
+ Potential Path Traversal (CWE-22).
+ User-controlled filename is concatenated with base directory without validation.
+ Attacker can use "../" to write files outside /tmp/reports/.
+ Use Paths.get(baseDir).resolve(filename).normalize() and verify the result starts with baseDir.
+ languages:
+ - java
+ severity: ERROR
+ metadata:
+ cwe: "CWE-22"
+ owasp: "A01:2021 - Broken Access Control"
+ confidence: HIGH
+
+ - id: javalin-ssrf-in-notify
+ patterns:
+ - pattern: |
+ URL $URL = new URL($USERINPUT);
+ ...
+ $URL.openConnection();
+ message: |
+ Potential Server-Side Request Forgery (CWE-918).
+ User-controlled URL is used to make server-side HTTP requests without validation.
+ Attacker can access internal services (localhost, 169.254.169.254, internal IPs).
+ Validate URL against allowlist and block private/internal IP ranges.
+ languages:
+ - java
+ severity: ERROR
+ metadata:
+ cwe: "CWE-918"
+ owasp: "A10:2021 - Server-Side Request Forgery"
+ confidence: HIGH
+
+ - id: javalin-info-disclosure-in-errors
+ pattern-either:
+ - pattern: |
+ ctx.status(400).result("Invalid data: " + e.getMessage())
+ - pattern: |
+ ctx.status(400).result(e.getMessage())
+ message: |
+ Potential Information Disclosure (CWE-209).
+ Exception messages are returned directly to the client, revealing internal logic.
+ Return generic error messages and log detailed errors server-side.
+ languages:
+ - java
+ severity: WARNING
+ metadata:
+ cwe: "CWE-209"
+ confidence: MEDIUM
\ No newline at end of file
diff --git a/semgrep-report.sarif b/semgrep-report.sarif
new file mode 100644
index 0000000..61e60df
--- /dev/null
+++ b/semgrep-report.sarif
@@ -0,0 +1 @@
+{"version":"2.1.0","runs":[{"invocations":[{"executionSuccessful":true,"toolExecutionNotifications":[{"descriptor":{"id":"SemgrepError"},"level":"error","message":{"text":"Invalid scanning root: /src"}}]}],"results":[],"tool":{"driver":{"name":"Semgrep OSS","rules":[],"semanticVersion":"1.159.0"}}}],"$schema":"https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/schemas/sarif-schema-2.1.0.json"}
\ No newline at end of file
diff --git a/semgrep1-report.sarif b/semgrep1-report.sarif
new file mode 100644
index 0000000..cb124ce
--- /dev/null
+++ b/semgrep1-report.sarif
@@ -0,0 +1 @@
+{"version":"2.1.0","runs":[{"invocations":[{"executionSuccessful":true,"toolExecutionNotifications":[]}],"results":[{"fingerprints":{"matchBasedId/v1":"requires login"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"src/main/java/ru/itmo/testing/lab4/controller/UserAnalyticsController.java","uriBaseId":"%SRCROOT%"},"region":{"endColumn":74,"endLine":74,"snippet":{"text":" ctx.status(400).result(\"Invalid data: \" + e.getMessage());"},"startColumn":17,"startLine":74}}}],"message":{"text":"Potential Information Disclosure (CWE-209).\nException messages are returned directly to the client, revealing internal logic.\nReturn generic error messages and log detailed errors server-side.\n"},"properties":{},"ruleId":"javalin-info-disclosure-in-errors"},{"fingerprints":{"matchBasedId/v1":"requires login"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"src/main/java/ru/itmo/testing/lab4/controller/UserAnalyticsController.java","uriBaseId":"%SRCROOT%"},"region":{"endColumn":74,"endLine":115,"snippet":{"text":" ctx.status(400).result(\"Invalid data: \" + e.getMessage());"},"startColumn":17,"startLine":115}}}],"message":{"text":"Potential Information Disclosure (CWE-209).\nException messages are returned directly to the client, revealing internal logic.\nReturn generic error messages and log detailed errors server-side.\n"},"properties":{},"ruleId":"javalin-info-disclosure-in-errors"},{"fingerprints":{"matchBasedId/v1":"requires login"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"src/main/java/ru/itmo/testing/lab4/controller/UserAnalyticsController.java","uriBaseId":"%SRCROOT%"},"region":{"endColumn":68,"endLine":154,"snippet":{"text":" File reportFile = new File(REPORTS_BASE_DIR + filename);"},"startColumn":31,"startLine":154}}}],"message":{"text":"Potential Path Traversal (CWE-22).\nUser-controlled filename is concatenated with base directory without validation.\nAttacker can use \"../\" to write files outside /tmp/reports/.\nUse Paths.get(baseDir).resolve(filename).normalize() and verify the result starts with baseDir.\n"},"properties":{},"ruleId":"javalin-path-traversal-in-export"},{"fingerprints":{"matchBasedId/v1":"requires login"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"src/main/java/ru/itmo/testing/lab4/controller/UserAnalyticsController.java","uriBaseId":"%SRCROOT%"},"region":{"endColumn":65,"endLine":181,"snippet":{"text":" URL url = new URL(callbackUrl);\n URLConnection connection = url.openConnection();"},"startColumn":17,"startLine":180}}}],"message":{"text":"Potential Server-Side Request Forgery (CWE-918).\nUser-controlled URL is used to make server-side HTTP requests without validation.\nAttacker can access internal services (localhost, 169.254.169.254, internal IPs).\nValidate URL against allowlist and block private/internal IP ranges.\n"},"properties":{},"ruleId":"javalin-ssrf-in-notify"}],"tool":{"driver":{"name":"Semgrep OSS","rules":[{"defaultConfiguration":{"level":"warning"},"fullDescription":{"text":"Potential Information Disclosure (CWE-209).\nException messages are returned directly to the client, revealing internal logic.\nReturn generic error messages and log detailed errors server-side.\n"},"help":{"markdown":"Potential Information Disclosure (CWE-209).\nException messages are returned directly to the client, revealing internal logic.\nReturn generic error messages and log detailed errors server-side.\n","text":"Potential Information Disclosure (CWE-209).\nException messages are returned directly to the client, revealing internal logic.\nReturn generic error messages and log detailed errors server-side.\n"},"id":"javalin-info-disclosure-in-errors","name":"javalin-info-disclosure-in-errors","properties":{"precision":"very-high","tags":["CWE-209","MEDIUM CONFIDENCE","security"]},"shortDescription":{"text":"Semgrep Finding: javalin-info-disclosure-in-errors"}},{"defaultConfiguration":{"level":"error"},"fullDescription":{"text":"Potential Path Traversal (CWE-22).\nUser-controlled filename is concatenated with base directory without validation.\nAttacker can use \"../\" to write files outside /tmp/reports/.\nUse Paths.get(baseDir).resolve(filename).normalize() and verify the result starts with baseDir.\n"},"help":{"markdown":"Potential Path Traversal (CWE-22).\nUser-controlled filename is concatenated with base directory without validation.\nAttacker can use \"../\" to write files outside /tmp/reports/.\nUse Paths.get(baseDir).resolve(filename).normalize() and verify the result starts with baseDir.\n","text":"Potential Path Traversal (CWE-22).\nUser-controlled filename is concatenated with base directory without validation.\nAttacker can use \"../\" to write files outside /tmp/reports/.\nUse Paths.get(baseDir).resolve(filename).normalize() and verify the result starts with baseDir.\n"},"id":"javalin-path-traversal-in-export","name":"javalin-path-traversal-in-export","properties":{"precision":"very-high","tags":["CWE-22","HIGH CONFIDENCE","OWASP-A01:2021 - Broken Access Control","security"]},"shortDescription":{"text":"Semgrep Finding: javalin-path-traversal-in-export"}},{"defaultConfiguration":{"level":"error"},"fullDescription":{"text":"Potential Server-Side Request Forgery (CWE-918).\nUser-controlled URL is used to make server-side HTTP requests without validation.\nAttacker can access internal services (localhost, 169.254.169.254, internal IPs).\nValidate URL against allowlist and block private/internal IP ranges.\n"},"help":{"markdown":"Potential Server-Side Request Forgery (CWE-918).\nUser-controlled URL is used to make server-side HTTP requests without validation.\nAttacker can access internal services (localhost, 169.254.169.254, internal IPs).\nValidate URL against allowlist and block private/internal IP ranges.\n","text":"Potential Server-Side Request Forgery (CWE-918).\nUser-controlled URL is used to make server-side HTTP requests without validation.\nAttacker can access internal services (localhost, 169.254.169.254, internal IPs).\nValidate URL against allowlist and block private/internal IP ranges.\n"},"id":"javalin-ssrf-in-notify","name":"javalin-ssrf-in-notify","properties":{"precision":"very-high","tags":["CWE-918","HIGH CONFIDENCE","OWASP-A10:2021 - Server-Side Request Forgery","security"]},"shortDescription":{"text":"Semgrep Finding: javalin-ssrf-in-notify"}},{"defaultConfiguration":{"level":"error"},"fullDescription":{"text":"Potential Reflected XSS (CWE-79).\nUser-controlled data (userName) is concatenated directly into HTML response without escaping.\nUse HTML encoding (e.g., StringEscapeUtils.escapeHtml4) or a template engine.\n"},"help":{"markdown":"Potential Reflected XSS (CWE-79).\nUser-controlled data (userName) is concatenated directly into HTML response without escaping.\nUse HTML encoding (e.g., StringEscapeUtils.escapeHtml4) or a template engine.\n","text":"Potential Reflected XSS (CWE-79).\nUser-controlled data (userName) is concatenated directly into HTML response without escaping.\nUse HTML encoding (e.g., StringEscapeUtils.escapeHtml4) or a template engine.\n"},"id":"javalin-xss-in-userprofile","name":"javalin-xss-in-userprofile","properties":{"precision":"very-high","tags":["CWE-79","HIGH CONFIDENCE","OWASP-A03:2021 - Injection","security"]},"shortDescription":{"text":"Semgrep Finding: javalin-xss-in-userprofile"}}],"semanticVersion":"1.159.0"}}}],"$schema":"https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/schemas/sarif-schema-2.1.0.json"}
\ No newline at end of file
diff --git a/src/test/java/ru/itmo/testing/lab4/pentest/InfoDisclosureTest.java b/src/test/java/ru/itmo/testing/lab4/pentest/InfoDisclosureTest.java
new file mode 100644
index 0000000..1027a25
--- /dev/null
+++ b/src/test/java/ru/itmo/testing/lab4/pentest/InfoDisclosureTest.java
@@ -0,0 +1,71 @@
+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.*;
+
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+class InfoDisclosureTest {
+
+ private static final int TEST_PORT = 7780;
+ 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("[SECURITY] Error messages disclose internal parsing details")
+ void errorMessagesDiscloseInternalDetails() throws Exception {
+ // Act: отправляем невалидный формат месяца
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(BASE_URL + "/monthlyActivity?userId=test&month=2026/01"))
+ .GET()
+ .build();
+ HttpResponse response = http.send(request, HttpResponse.BodyHandlers.ofString());
+
+ // Assert: уязвимость подтверждена — ответ содержит детали парсинга
+ assertEquals(400, response.statusCode());
+ assertTrue(response.body().contains("could not be parsed"),
+ "FAIL: Error message discloses internal parsing details.");
+ assertTrue(response.body().contains("Text '2026/01'"),
+ "FAIL: User input reflected in error message with parsing context.");
+ }
+
+ @Test
+ @Order(2)
+ @DisplayName("[SECURITY] Error message discloses business logic for non-existent user")
+ void errorMessageDisclosesBusinessLogic() throws Exception {
+ // Act: запрашиваем несуществующего пользователя
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(BASE_URL + "/monthlyActivity?userId=nonexistent&month=2026-01"))
+ .GET()
+ .build();
+ HttpResponse response = http.send(request, HttpResponse.BodyHandlers.ofString());
+
+ // Assert: уязвимость подтверждена — раскрывается информация о сессиях
+ assertEquals(400, response.statusCode());
+ assertTrue(response.body().contains("No sessions found for user"),
+ "FAIL: Error message discloses that user exists but has no sessions.");
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/ru/itmo/testing/lab4/pentest/MissingAuthPentestTest.java b/src/test/java/ru/itmo/testing/lab4/pentest/MissingAuthPentestTest.java
new file mode 100644
index 0000000..03262cb
--- /dev/null
+++ b/src/test/java/ru/itmo/testing/lab4/pentest/MissingAuthPentestTest.java
@@ -0,0 +1,86 @@
+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.*;
+
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+class MissingAuthPentestTest {
+
+ private static final int TEST_PORT = 7781;
+ 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("[SECURITY] Any user can access another user's data")
+ void anyUserCanAccessAnotherUsersData() throws Exception {
+ // Arrange: создаем двух пользователей
+ http.send(HttpRequest.newBuilder()
+ .uri(URI.create(BASE_URL + "/register?userId=alice&userName=Alice"))
+ .POST(HttpRequest.BodyPublishers.noBody())
+ .build(), HttpResponse.BodyHandlers.ofString());
+
+ http.send(HttpRequest.newBuilder()
+ .uri(URI.create(BASE_URL + "/register?userId=bob&userName=Bob"))
+ .POST(HttpRequest.BodyPublishers.noBody())
+ .build(), HttpResponse.BodyHandlers.ofString());
+
+ // Добавляем сессию Alice
+ http.send(HttpRequest.newBuilder()
+ .uri(URI.create(BASE_URL + "/recordSession?userId=alice&loginTime=2026-01-15T10:00:00&logoutTime=2026-01-15T11:00:00"))
+ .POST(HttpRequest.BodyPublishers.noBody())
+ .build(), HttpResponse.BodyHandlers.ofString());
+
+ // Act: Bob (злоумышленник) запрашивает данные Alice
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(BASE_URL + "/totalActivity?userId=alice"))
+ .GET()
+ .build();
+ HttpResponse response = http.send(request, HttpResponse.BodyHandlers.ofString());
+
+ // Assert: уязвимость подтверждена — Bob получил данные Alice
+ assertEquals(200, response.statusCode());
+ assertTrue(response.body().contains("60 minutes") || response.body().contains("Total activity:"),
+ "FAIL: Missing authentication allows access to another user's data.");
+ }
+
+ @Test
+ @Order(2)
+ @DisplayName("[SECURITY] Any user can add sessions for another user")
+ void anyUserCanAddSessionsForAnotherUser() throws Exception {
+ // Act: Bob добавляет сессию для Alice
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(BASE_URL + "/recordSession?userId=alice&loginTime=2026-01-15T12:00:00&logoutTime=2026-01-15T13:00:00"))
+ .POST(HttpRequest.BodyPublishers.noBody())
+ .build();
+ HttpResponse response = http.send(request, HttpResponse.BodyHandlers.ofString());
+
+ // Assert: уязвимость подтверждена — сессия добавлена
+ assertEquals(200, response.statusCode());
+ assertTrue(response.body().contains("Session recorded"),
+ "FAIL: Missing authentication allows modifying another user's data.");
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/ru/itmo/testing/lab4/pentest/PathTraversalTest.java b/src/test/java/ru/itmo/testing/lab4/pentest/PathTraversalTest.java
new file mode 100644
index 0000000..814f265
--- /dev/null
+++ b/src/test/java/ru/itmo/testing/lab4/pentest/PathTraversalTest.java
@@ -0,0 +1,67 @@
+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 java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+
+class PathTraversalTest {
+
+ private static final int TEST_PORT = 7000;
+ private static final String BASE_URL = "http://localhost:" + TEST_PORT;
+ private static final Path EVIL_FILE = Paths.get("/tmp/hack.txt");
+
+ 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();
+ }
+
+ @BeforeEach
+ void cleanup() throws Exception {
+ Files.deleteIfExists(EVIL_FILE);
+ }
+
+ @Test
+ @DisplayName("[SECURITY] Path Traversal allows writing file outside /tmp/reports/")
+ void pathTraversalWritesOutsideReportsDir() throws Exception {
+ // Arrange: регистрируем пользователя
+ HttpRequest registerReq = HttpRequest.newBuilder()
+ .uri(URI.create(BASE_URL + "/register?userId=test&userName=Test"))
+ .POST(HttpRequest.BodyPublishers.noBody())
+ .build();
+ http.send(registerReq, HttpResponse.BodyHandlers.ofString());
+
+ // Act: пытаемся записать файл в /tmp/ через Path Traversal
+ String payload = "../hack.txt";
+ HttpRequest exportReq = HttpRequest.newBuilder()
+ .uri(URI.create(BASE_URL + "/exportReport?userId=test&filename=" + payload))
+ .GET()
+ .build();
+ HttpResponse response = http.send(exportReq, HttpResponse.BodyHandlers.ofString());
+
+ // Assert: уязвимость подтверждена — файл создался за пределами /tmp/reports/
+ assertTrue(Files.exists(EVIL_FILE),
+ "FAIL: File was created outside /tmp/reports/. Path Traversal confirmed.");
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/ru/itmo/testing/lab4/pentest/SSRTTest.java b/src/test/java/ru/itmo/testing/lab4/pentest/SSRTTest.java
new file mode 100644
index 0000000..b2502e5
--- /dev/null
+++ b/src/test/java/ru/itmo/testing/lab4/pentest/SSRTTest.java
@@ -0,0 +1,64 @@
+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.URLEncoder;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class SSRTTest {
+
+ private static final int TEST_PORT = 7779;
+ 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("[SECURITY] SSRF allows request to localhost")
+ void ssrfAllowsLocalhostRequest() throws Exception {
+ // Arrange: регистрируем пользователя
+ HttpRequest registerReq = HttpRequest.newBuilder()
+ .uri(URI.create(BASE_URL + "/register?userId=test&userName=Test"))
+ .POST(HttpRequest.BodyPublishers.noBody())
+ .build();
+ http.send(registerReq, HttpResponse.BodyHandlers.ofString());
+
+ // Act: пытаемся сделать запрос к localhost (сами к себе)
+ String internalUrl = "http://127.0.0.1:" + TEST_PORT + "/userProfile?userId=test";
+ String encodedUrl = URLEncoder.encode(internalUrl, StandardCharsets.UTF_8);
+ HttpRequest notifyReq = HttpRequest.newBuilder()
+ .uri(URI.create(BASE_URL + "/notify?userId=test&callbackUrl=" + encodedUrl))
+ .POST(HttpRequest.BodyPublishers.noBody())
+ .build();
+ HttpResponse response = http.send(notifyReq, HttpResponse.BodyHandlers.ofString());
+
+ // Assert: уязвимость подтверждена — сервер сделал запрос к localhost
+ assertTrue(response.body().contains("Profile: Test"),
+ "FAIL: SSRF confirmed — server made request to localhost and returned internal data.");
+ }
+
+}
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..3cd0e17 100644
--- a/src/test/java/ru/itmo/testing/lab4/pentest/XssPentestTest.java
+++ b/src/test/java/ru/itmo/testing/lab4/pentest/XssPentestTest.java
@@ -2,6 +2,7 @@
import io.javalin.Javalin;
import org.junit.jupiter.api.*;
+import org.junit.platform.commons.util.StringUtils;
import ru.itmo.testing.lab4.controller.UserAnalyticsController;
import java.net.URI;
From a3344f489737128e02d69f6f1331c8a3c1e026e3 Mon Sep 17 00:00:00 2001
From: DenisStepanidenko <110686828+DenisStepanidenko@users.noreply.github.com>
Date: Tue, 14 Apr 2026 16:13:46 +0300
Subject: [PATCH 9/9] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 7b2fcca..f96c623 100644
--- a/README.md
+++ b/README.md
@@ -538,7 +538,7 @@ public class SSRTTest {
---
-#### 🔴 Finding #4 — Server-Side Request Forgery (SSRF)
+#### 🔴 Finding #4 — Information Disclosure through Error Messages
| Поле | Значение |
|------|----------|