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 .idea/.name

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/encodings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

129 changes: 115 additions & 14 deletions README.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ repositories {
dependencies {
implementation 'io.javalin:javalin:6.3.0'
implementation 'org.slf4j:slf4j-simple:2.0.13'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.2'

testImplementation platform('org.junit:junit-bom:6.0.0')
testImplementation 'org.junit.jupiter:junit-jupiter'
Expand Down
1 change: 1 addition & 0 deletions semgrep-report.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"version":"1.159.0","results":[],"errors":[],"paths":{"scanned":["src\\main\\java\\ru\\itmo\\testing\\lab4\\controller\\UserAnalyticsController.java","src\\main\\java\\ru\\itmo\\testing\\lab4\\Main.java","src\\main\\java\\ru\\itmo\\testing\\lab4\\profilingdemo\\ProfilingDemoMain.java","src\\main\\java\\ru\\itmo\\testing\\lab4\\profilingdemo\\ProfilingWorkload.java","src\\main\\java\\ru\\itmo\\testing\\lab4\\service\\UserAnalyticsService.java","src\\main\\java\\ru\\itmo\\testing\\lab4\\service\\UserStatusService.java"]},"time":{"rules":[],"rules_parse_time":0.12845182418823242,"profiling_times":{"config_time":2.1372992992401123,"core_time":0.6763730049133301,"ignores_time":0.00011754035949707031,"total_time":2.841381549835205},"parsing_time":{"total_time":0.0,"per_file_time":{"mean":0.0,"std_dev":0.0},"very_slow_stats":{"time_ratio":0.0,"count_ratio":0.0},"very_slow_files":[]},"scanning_time":{"total_time":0.27506208419799805,"per_file_time":{"mean":0.04584368069966634,"std_dev":0.00022842930818316946},"very_slow_stats":{"time_ratio":0.0,"count_ratio":0.0},"very_slow_files":[]},"matching_time":{"total_time":0.0,"per_file_and_rule_time":{"mean":0.0,"std_dev":0.0},"very_slow_stats":{"time_ratio":0.0,"count_ratio":0.0},"very_slow_rules_on_files":[]},"tainting_time":{"total_time":0.0,"per_def_and_rule_time":{"mean":0.0,"std_dev":0.0},"very_slow_stats":{"time_ratio":0.0,"count_ratio":0.0},"very_slow_rules_on_defs":[]},"fixpoint_timeouts":[],"prefiltering":{"project_level_time":0.0,"file_level_time":0.0,"rules_with_project_prefilters_ratio":0.0,"rules_with_file_prefilters_ratio":0.95,"rules_selected_ratio":0.10833333333333334,"rules_matched_ratio":0.10833333333333334},"targets":[],"total_bytes":0,"max_memory_bytes":208133696},"engine_requested":"OSS","skipped_rules":[],"profiling_results":[]}
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package ru.itmo.testing.lab4.pentest;

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

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

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

/**
* =============================================================================
* ПЕНТЕСТ-ОТЧЁТ: CWE-306 - Отсутствие аутентификации
* =============================================================================
* <p>
* Компонент: Все эндпоинты (GET /totalActivity, POST /recordSession, GET /monthlyActivity, и т.д.)
* CWE: CWE-306 (Missing Authentication for Critical Function)
* CVSS v3.1: 7.5 (High) - AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
* Статус: CONFIRMED
* <p>
* ОПИСАНИЕ:
* Приложение не проверяет аутентификацию пользователя перед выполнением любых действий.
* Любой внешний злоумышленник может выполнять запросы от имени любого пользователя,
* подставляя произвольный userId в query-параметры.
* <p>
* ВЕКТОР АТАКИ:
* 1. Злоумышленник регистрирует своего пользователя или находит существующего
* 2. Выполняет запрос без какой-либо аутентификации:
* GET /totalActivity?userId=alice
* → Сервер возвращает данные пользователя alice без проверки прав.
* <p>
* ВЛИЯНИЕ:
* - Получение данных любого пользователя
* - Запись фейковых сессий от имени любого пользователя
* - Эксплуатация всех остальных уязвимостей без ограничений
* <p>
* МЕРЫ ЗАЩИТЫ:
* - Добавить механизм аутентификации (JWT, Session, OAuth2)
* - Добавить before-обработчик для проверки токена
* - Использовать userId из аутентифицированного контекста, а не из параметров запроса
* =============================================================================
*/
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class AuthenticationPentestTest {

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

private static Javalin app;
private static HttpClient http;

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

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

@Test
@Order(1)
@DisplayName("[RECON] Регистрация пользователя для тестирования")
void registerTestUser() throws Exception {
HttpResponse<String> response = send("POST", "/register?userId=alice&userName=Alice");
assertEquals(200, response.statusCode());
assertTrue(response.body().contains("User registered"));
}

@Test
@Order(2)
@DisplayName("[RECON] /totalActivity доступен без аутентификации - УЯЗВИМОСТЬ ПОДТВЕРЖДЕНА")
void totalActivityAccessibleWithoutAuth() throws Exception {
send("POST", "/recordSession?userId=alice&loginTime=2024-01-01T09:00:00&logoutTime=2024-01-01T17:00:00");

HttpResponse<String> response = send("GET", "/totalActivity?userId=alice");

assertEquals(200, response.statusCode(),
"УЯЗВИМОСТЬ: /totalActivity доступен без аутентификации, должен быть защищён");
assertTrue(response.body().contains("Total activity"),
"Данные пользователя возвращаются без аутентификации");
}

@Test
@Order(3)
@DisplayName("[RECON] /recordSession доступен без аутентификации - УЯЗВИМОСТЬ ПОДТВЕРЖДЕНА")
void recordSessionAccessibleWithoutAuth() throws Exception {
HttpResponse<String> response = send("POST",
"/recordSession?userId=alice&loginTime=2024-01-02T09:00:00&logoutTime=2024-01-02T17:00:00");

assertEquals(200, response.statusCode(),
"УЯЗВИМОСТЬ: /recordSession доступен без аутентификации, должен быть защищён");
assertTrue(response.body().contains("Session recorded"),
"Сессия записана без аутентификации");
}

@Test
@Order(4)
@DisplayName("[RECON] /monthlyActivity доступен без аутентификации - УЯЗВИМОСТЬ ПОДТВЕРЖДЕНА")
void monthlyActivityAccessibleWithoutAuth() throws Exception {
HttpResponse<String> response = send("GET", "/monthlyActivity?userId=alice&month=2024-01");

assertEquals(200, response.statusCode(),
"УЯЗВИМОСТЬ: /monthlyActivity доступен без аутентификации, должен быть защищён");
}

@Test
@Order(5)
@DisplayName("[RECON] /exportReport доступен без аутентификации - УЯЗВИМОСТЬ ПОДТВЕРЖДЕНА")
void exportReportAccessibleWithoutAuth() throws Exception {
HttpResponse<String> response = send("GET", "/exportReport?userId=alice&filename=test.txt");

assertEquals(200, response.statusCode(),
"УЯЗВИМОСТЬ: /exportReport доступен без аутентификации, должен быть защищён");
}

@Test
@Order(6)
@DisplayName("[RECON] /notify доступен без аутентификации - УЯЗВИМОСТЬ ПОДТВЕРЖДЕНА")
void notifyAccessibleWithoutAuth() throws Exception {
HttpResponse<String> response = send("POST",
"/notify?userId=alice&callbackUrl=https://httpbin.org/get");

assertEquals(200, response.statusCode(),
"УЯЗВИМОСТЬ: /notify доступен без аутентификации, должен быть защищён");
}

@Test
@Order(7)
@DisplayName("[EXPLOIT] Можно получить данные другого пользователя без аутентификации")
void canAccessOtherUserDataWithoutAuth() throws Exception {
send("POST", "/register?userId=bob&userName=Bob");
send("POST", "/recordSession?userId=bob&loginTime=2024-01-01T09:00:00Z&logoutTime=2024-01-01T17:00:00Z");

HttpResponse<String> response = send("GET", "/totalActivity?userId=bob");

assertEquals(200, response.statusCode(),
"УЯЗВИМОСТЬ: данные пользователя bob доступны без аутентификации");
assertTrue(response.body().contains("Total activity"),
"Возвращаются данные чужого пользователя");
}

@Test
@Order(8)
@DisplayName("[EXPLOIT] Можно записать сессию от имени другого пользователя без аутентификации")
void canRecordSessionForOtherUserWithoutAuth() throws Exception {
HttpResponse<String> response = send("POST",
"/recordSession?userId=bob&loginTime=2024-01-03T09:00:00&logoutTime=2024-01-03T17:00:00");

assertEquals(200, response.statusCode(),
"УЯЗВИМОСТЬ: можно записать сессию от имени другого пользователя без аутентификации");
assertTrue(response.body().contains("Session recorded"),
"Сессия записана от имени чужого пользователя");
}

@Test
@Order(9)
@DisplayName("[BOUNDARY] Несуществующий userId в /totalActivity возвращает 0")
void nonExistentUserInTotalActivity() throws Exception {
HttpResponse<String> response = send("GET", "/totalActivity?userId=nonexistent");

assertTrue(response.body().contains("0 minutes") || response.body().contains("0"),
"Несуществующий пользователь должен возвращать 0 активности");
}

private 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());
}
}
126 changes: 126 additions & 0 deletions src/test/java/ru/itmo/testing/lab4/pentest/IdorPentestTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package ru.itmo.testing.lab4.pentest;

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

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

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

/**
* =============================================================================
* ПЕНТЕСТ-ОТЧЕТ: CWE-639 - IDOR (Insecure Direct Object Reference)
* =============================================================================
* <p>
* Компонент: POST /recordSession, GET /totalActivity, GET /monthlyActivity
* CWE: CWE-639 (Authorization Bypass Through User-Controlled Key)
* CVSS v3.1: 5.3 (Medium) - AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
* Статус: CONFIRMED
* <p>
* ОПИСАНИЕ:
* Параметр userId передаётся через query-параметры и не проверяется на соответствие
* аутентифицированному пользователю. Любой может подставить чужой userId и получить
* или модифицировать его данные.
* <p>
* ВЕКТОР АТАКИ:
* 1. Злоумышленник узнаёт, что существует пользователь "bob"
* 2. Выполняет запрос с подменой userId:
* GET /totalActivity?userId=bob
* -> Сервер возвращает данные пользователя bob, хотя злоумышленник не авторизован для этого.
* <p>
* ВЛИЯНИЕ:
* - Получение активности любого пользователя
* - Запись фейковых сессий от имени других пользователей
* - Искажение аналитики и отчётов
* <p>
* МЕРЫ ЗАЩИТЫ:
* - Извлекать userId из аутентифицированного контекста (JWT, сессия)
* - Не доверять userId из параметров запроса
* - Добавить проверку прав: if (!userId.equals(requestedUserId) && !isAdmin())
* =============================================================================
*/
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class IdorPentestTest {

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

private static Javalin app;
private static HttpClient http;

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

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

@Test
@Order(1)
@DisplayName("[RECON] Регистрация пользователей для тестирования IDOR")
void registerTestUsers() throws Exception {
send("POST", "/register?userId=alice&userName=Alice");
send("POST", "/register?userId=bob&userName=Bob");

send("POST", "/recordSession?userId=alice&loginTime=2024-01-01T09:00:00&logoutTime=2024-01-01T12:00:00");
send("POST", "/recordSession?userId=bob&loginTime=2024-01-01T13:00:00&logoutTime=2024-01-01T18:00:00");
}

@Test
@Order(2)
@DisplayName("[EXPLOIT] IDOR в /totalActivity - можно получить активность другого пользователя")
void idorInTotalActivity() throws Exception {
HttpResponse<String> response = send("GET", "/totalActivity?userId=bob");

assertTrue(response.body().contains("300 minutes") || response.body().contains("Total activity"),
"Данные пользователя bob доступны через подмену userId - IDOR уязвимость");
}

@Test
@Order(3)
@DisplayName("[EXPLOIT] IDOR в /monthlyActivity - можно получить активность другого пользователя")
void idorInMonthlyActivity() throws Exception {
HttpResponse<String> response = send("GET", "/monthlyActivity?userId=bob&month=2024-01");

assertEquals(200, response.statusCode(),
"Данные monthlyActivity для bob доступны через подмену userId - IDOR уязвимость");
}

@Test
@Order(4)
@DisplayName("[EXPLOIT] IDOR в /recordSession - можно записать сессию от имени другого пользователя")
void idorInRecordSession() throws Exception {
HttpResponse<String> response = send("POST",
"/recordSession?userId=alice&loginTime=2024-01-02T09:00:00&logoutTime=2024-01-02T17:00:00");

assertEquals(200, response.statusCode(),
"Можно записать сессию от имени другого пользователя - IDOR уязвимость");
}

@Test
@Order(5)
@DisplayName("[BOUNDARY] Несуществующий userId в /totalActivity возвращает 0")
void nonExistentUserInTotalActivity() throws Exception {
HttpResponse<String> response = send("GET", "/totalActivity?userId=nonexistent");

assertTrue(response.body().contains("0 minutes") || response.body().contains("0"),
"Несуществующий пользователь должен возвращать 0 активности");
}

private 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());
}
}
Loading