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 @@ +[![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](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" > Данный пример — лишь отправная точка. Исследуйте **все** эндпоинты самостоятельно. +image + + --- ### Этап 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: + +image + +Пример XSS alert: image 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 | Поле | Значение | |------|----------|