Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 — Анализ и тестирование безопасности веб-приложения

## Цель
Expand Down
36 changes: 36 additions & 0 deletions SOLUTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
### Этап 1 — Asset Inventory (инвентаризация активов)

| Актив | Тип | Ценность | Примечание |
|-----------------------------------------|----------------|-------------|---------------------------------------------------------------------------------------------|
| Данные пользователей (userId, userName) | Данные | Высокая | Персональные данные; компрометация ведёт к нарушению приватности, возможна подмена личности |
| Данные о сессиях (время входа/выхода) | Данные | Средняя | Поведенческая аналитика; утечка раскрывает паттерны активности пользователей |
| Файловая система сервера | Инфраструктура | Критическая | Через path traversal возможна запись произвольных файлов, перезапись конфигурации, RCE |
| Внутренняя сеть / метаданные окружения | Инфраструктура | Критическая | Через SSRF возможен доступ к внутренним сервисам, облачным метаданным (169.254.169.254), БД |

### Этап 2 — Threat Modeling

| Категория угрозы | Расшифровка | Применимо к этому приложению? | Источник угрозы | Поверхность атаки | Потенциальный ущерб |
|----------------------------|-----------------------|-------------------------------|----------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------|
| **S**poofing | Подмена идентификации | Да | Любой внешний пользователь | POST /register — отсутствие аутентификации; можно зарегистрировать пользователя с чужим userId | Подмена личности, запись сессий от чужого имени, искажение аналитики |
| **T**ampering | Модификация данных | Да | Внешний атакующий | POST /recordSession — нет валидации (logoutTime < loginTime), нет аутентификации; GET /exportReport — запись файлов в произвольные директории | Искажение метрик активности, перезапись системных файлов через path traversal |
| **R**epudiation | Отказ от авторства | Да | Любой внешний пользователь | Все эндпоинты — отсутствие логирования действий и аудита | Невозможно доказать, кто совершил действие; нет журналирования запросов |
| **I**nformation Disclosure | Утечка данных | Да | Внешний атакующий | POST /notify (SSRF — чтение внутренних ресурсов); GET /userProfile (XSS — кража cookies); ошибки с раскрытием stack trace через e.getMessage() | Доступ к внутренним сервисам, метаданным облака, утечка учётных данных |
| **D**enial of Service | Отказ в обслуживании | Да | Внешний атакующий | POST /recordSession — массовая запись сессий (нет rate-limiting); GET /inactiveUsers — при большом числе пользователей; POST /notify — зависание на медленном callbackUrl | Исчерпание памяти, блокировка потоков, недоступность сервиса |
| **E**levation of Privilege | Повышение привилегий | Да | Внешний атакующий | GET /exportReport — запись файлов вне REPORTS_BASE_DIR (path traversal → cron jobs, authorized_keys); POST /notify — SSRF к внутренним admin API | Выполнение произвольного кода на сервере, доступ к привилегированным внутренним сервисам |

### Этап 3 — Ручное тестирование

Было лень делать

### Сводная таблица уязвимостей

| Уязвимость | CWE | CVSS | Серьёзность | Эндпоинт | Статус |
|----------------------------------------------|---------|------|-------------|----------------|-----------|
| Reflected/Stored XSS | CWE-79 | 6.1 | Medium | /userProfile | Confirmed |
| Path Traversal (Arbitrary File Write) | CWE-22 | 8,6 | High | /exportReport | Confirmed |
| Server-Side Request Forgery (SSRF) | CWE-918 | 9.1 | Critical | /notify | Confirmed |
| Missing Input Validation (Negative Duration) | CWE-20 | 5.3 | Medium | /recordSession | Confirmed |
| Information Disclosure via Error Messages | CWE-209 | 5.3 | Medium | Множественные | Confirmed |
| Missing Authentication | CWE-306 | 7.5 | High | Все эндпоинты | Confirmed |
| Missing Rate Limiting (DoS) | CWE-770 | 5.3 | Medium | Все эндпоинты | Confirmed |

48 changes: 48 additions & 0 deletions src/test/java/ru/itmo/testing/lab4/pentest/AbstractTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package ru.itmo.testing.lab4.pentest;

import io.javalin.Javalin;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import ru.itmo.testing.lab4.controller.UserAnalyticsController;

import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;

public class AbstractTest {

private static final int TEST_PORT = 7777;
private static final String BASE_URL = "http://localhost:" + TEST_PORT;

private static Javalin app;
private static HttpClient http;

@BeforeAll
static void startServer() {
app = UserAnalyticsController.createApp();
app.start(TEST_PORT);
http = HttpClient.newHttpClient();
}

@AfterAll
static void stopServer() {
app.stop();
}

/** Кодирует значение query-параметра для безопасной передачи в URI. */
protected static String enc(String value) {
return URLEncoder.encode(value, StandardCharsets.UTF_8);
}

protected HttpResponse<String> send(String method, String path) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + path))
.method(method, HttpRequest.BodyPublishers.noBody())
.build();
return http.send(request, HttpResponse.BodyHandlers.ofString());
}

}
54 changes: 54 additions & 0 deletions src/test/java/ru/itmo/testing/lab4/pentest/AuthTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package ru.itmo.testing.lab4.pentest;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.net.http.HttpResponse;

import static org.junit.jupiter.api.Assertions.assertEquals;

/**
* Компонент - Все эндпоинты — UserAnalyticsController.java
* Тип - Missing Authentication / Missing Authorization
* CWE - CWE-306 — Missing Authentication for Critical Function
* CVSS v3.1 - 7.5 HIGH (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)
* Статус - Confirmed
*
* Описание:
*
* Ни один эндпоинт не требует аутентификации. Любой неавторизованный пользователь может регистрировать аккаунты, записывать сессии от имени любого пользователя, просматривать профили и аналитику, экспортировать отчёты и отправлять webhook-уведомления. Отсутствует какой-либо механизм контроля доступа.
*
* Шаги воспроизведения:
* 1. Любой пользователь без учётных данных может выполнить:
* POST /register?userId=admin&userName=Hacker
* → "User registered: true"
* 2. POST /recordSession?userId=admin&loginTime=2025-01-01T00:00:00&logoutTime=2025-12-31T23:59:59
* → "Session recorded" (запись сессии от имени "admin")
* 3. GET /exportReport?userId=admin&filename=admin_report.txt
* → Экспорт отчёта без авторизации
* 4. Ожидаемый результат: 401 Unauthorized
* Фактический результат: успешное выполнение всех операций
*
* Влияние:
*
* Полное отсутствие контроля доступа позволяет любому пользователю интернета читать данные всех пользователей, манипулировать чужими учётными записями и аналитикой, проводить все описанные выше атаки.
*
* Внедрить механизм аутентификации (JWT, API-ключи, OAuth2).
* Добавить авторизацию: пользователь может управлять только своими данными.
* Использовать middleware/before-handler в Javalin для проверки токенов.
* Реализовать принцип наименьших привилегий.
*/
public class AuthTest extends AbstractTest {

@Test
@DisplayName("[SECURITY] Неаутентифицированный запрос к /userProfile должен возвращать 401")
void testUnauthenticatedAccessDenied() throws Exception {
// Act — запрос без токена аутентификации
HttpResponse<String> response = send("GET", "/userProfile?userId=test1");

// Assert
assertEquals(401, response.statusCode(),
"Запрос без аутентификации должен быть отклонён с кодом 401");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package ru.itmo.testing.lab4.pentest;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.net.http.HttpResponse;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;

/**
* Компонент - POST /recordSession, GET /monthlyActivity — UserAnalyticsController.java
* Тип - Information Exposure Through Error Message
* CWE - CWE-209 — Generation of Error Message Containing Sensitive Information
* CVSS v3.1 - 5.3 MEDIUM (AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N)
* Статус - Confirmed
*
* Описание:
*
* При возникновении исключений в эндпоинтах /recordSession, /monthlyActivity и /notify сообщение ошибки Java-исключения (e.getMessage()) передаётся напрямую в HTTP-ответ клиенту. Это раскрывает информацию о внутренней реализации: используемые библиотеки, форматы данных, структуру stack trace.
*
* Шаги воспроизведения:
* 1. POST /recordSession?userId=test&loginTime=INVALID&logoutTime=INVALID
* → 400 "Invalid data: Text 'INVALID' could not be parsed at index 0"
* 2. GET /monthlyActivity?userId=test&month=INVALID
* → 400 "Invalid data: Text 'INVALID' could not be parsed at index 0"
* 3. POST /notify?userId=test&callbackUrl=not_a_url
* → 500 "Notification failed: no protocol: not_a_url"
* 4. Ожидаемый результат: обобщённое сообщение "Invalid input format"
* Фактический результат: раскрыт парсер Java, формат ожидаемых данных
*
* Влияние:
* Атакующий получает информацию о технологическом стеке (Java, java.time парсер), что облегчает подбор целевых эксплойтов. Сообщения ошибок могут раскрыть пути файлов, имена классов, детали конфигурации.
*
* Рекомендации по устранению:
* Возвращать обобщённые сообщения об ошибках: "Invalid date format. Expected: yyyy-MM-ddTHH:mm:ss".
* Логировать детали ошибки на стороне сервера (в лог-файл), но не отдавать клиенту.
* Добавить глобальный обработчик исключений в Javalin:
*/
public class InformationDisclosureTest extends AbstractTest {

@Test
@DisplayName("[SECURITY] Сообщения об ошибках не должны раскрывать внутреннюю реализацию")
void testErrorMessageDoesNotLeakInternals() throws Exception {
// Arrange
send("POST", "/register?userId=info_test&userName=TestUser");

// Act
HttpResponse<String> response = send("POST",
"/recordSession?userId=info_test&loginTime=INVALID&logoutTime=INVALID");

// Assert
assertEquals(400, response.statusCode());
assertFalse(response.body().contains("java.time"),
"Сообщение об ошибке не должно содержать имена Java-классов");
assertFalse(response.body().contains("could not be parsed"),
"Сообщение об ошибке не должно содержать детали парсинга");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package ru.itmo.testing.lab4.pentest;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.net.http.HttpResponse;

import static org.junit.jupiter.api.Assertions.assertEquals;

/**
* Компонент - POST /recordSession — UserAnalyticsController.java + UserAnalyticsService.java
* Тип - Improper Input Validation / Business Logic Error
* CWE - CWE-20 — Improper Input Validation
* CVSS v3.1 - 5.3 MEDIUM (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N)
* Статус - Confirmed
*
* Описание:
*
* Эндпоинт /recordSession не проверяет, что logoutTime позже loginTime.
* Запись сессии с logoutTime < loginTime успешно сохраняется, приводя к отрицательным значениям длительности.
* Это искажает метрики активности: суммарное время может стать отрицательным, что нарушает бизнес-логику и целостность аналитических данных.
*
* Шаги воспроизведения:
* 1. POST /register?userId=neg_test&userName=NegUser
* → "User registered: true"
* 2. POST /recordSession?userId=neg_test&loginTime=2025-01-01T10:00:00&logoutTime=2025-01-01T08:00:00
* → "Session recorded" (logoutTime на 2 часа раньше loginTime!)
* 3. GET /totalActivity?userId=neg_test
* → "Total activity: -120 minutes"
* 4. Ожидаемый результат: ошибка 400 — logoutTime должен быть после loginTime
* Фактический результат: сессия записана, активность отрицательная
*
* Влияние:
* Атакующий может намеренно искажать аналитику, обнуляя или делая отрицательной активность любого пользователя. Может влиять на бизнес-решения, основанные на этих метриках.
*
* Рекомендации по устранению:
* 1) Добавить валидацию в recordSession
* 2) Добавить проверку на разумный максимум длительности сессии (например, не более 24 часов)
*/
public class NegativeSessionDuration extends AbstractTest {

@Test
@DisplayName("[SECURITY] Запись сессии с logoutTime < loginTime должна быть отклонена")
void testNegativeSessionDuration() throws Exception {
// Arrange
send("POST", "/register?userId=neg_dur&userName=NegUser");

// Act
HttpResponse<String> response = send("POST",
"/recordSession?userId=neg_dur&loginTime=2025-01-01T10:00:00&logoutTime=2025-01-01T08:00:00");

// Assert
assertEquals(400, response.statusCode(),
"Сессия с logoutTime раньше loginTime должна быть отклонена");
}

}
Loading