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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
.gradle
build/

.venv/
.semgrep-cache/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
Expand Down
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
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
plugins {
id 'java'
id 'application'
}

application {
mainClass = 'ru.itmo.testing.lab4.Main'
}

group = 'ru.itmo'
Expand All @@ -11,6 +16,7 @@ repositories {

dependencies {
implementation 'io.javalin:javalin:6.3.0'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.2'
implementation 'org.slf4j:slf4j-simple:2.0.13'

testImplementation platform('org.junit:junit-bom:6.0.0')
Expand Down
36 changes: 36 additions & 0 deletions scripts/run_semgrep_stage4.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env bash

set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"

SEM="${ROOT}/.venv/bin/semgrep"
if [[ ! -x "$SEM" ]]; then
python3 -m venv .venv
.venv/bin/pip install -q semgrep
fi

if [[ ! -d /tmp/semgrep-rules/java ]]; then
rm -rf /tmp/semgrep-rules
git clone --depth 1 https://github.com/semgrep/semgrep-rules.git /tmp/semgrep-rules
fi
OWASP_YAML="${ROOT}/.semgrep-cache/p-owasp-top-ten.yaml"
mkdir -p "${ROOT}/.semgrep-cache"
if [[ ! -s "$OWASP_YAML" ]]; then
curl -sS -L --max-time 180 "https://semgrep.dev/c/p/owasp-top-ten" -o "$OWASP_YAML"
fi

run_local() {
"$SEM" --metrics off --disable-version-check \
--config /tmp/semgrep-rules/java \
--config "$OWASP_YAML" \
"$@"
}

echo "=== Текстовый отчёт (stdout + semgrep-output.txt) ==="
run_local src/ 2>&1 | tee semgrep-output.txt

echo ""
echo "=== SARIF → semgrep-report.sarif ==="
run_local --sarif -o semgrep-report.sarif src/
echo "Готово."
86 changes: 86 additions & 0 deletions scripts/stage3_manual_curls.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env bash

set -euo pipefail
B="http://localhost:7000"

curlq() {
local desc="$1"
shift
echo "########## $desc ##########"
echo "Команда: curl -sS -D - -o - $*"
echo "---"
curl -sS -D - -o - "$@" 2>&1 || echo "(curl exit: $?)"
echo ""
echo ""
}

# --- Подготовка данных ---
curlq "SETUP register stage3_base / Alice" -X POST "$B/register?userId=stage3_base&userName=Alice"
curlq "SETUP register stage3_xss / script name" -X POST "$B/register?userId=stage3_xss&userName=%3Cscript%3Ealert(1)%3C%2Fscript%3E"
curlq "SETUP register stage3_quote / quote in name" -X POST "$B/register?userId=stage3_quote&userName=O%27Reilly%22%3C%3E"
curlq "SETUP register stage3_sess / for sessions" -X POST "$B/register?userId=stage3_sess&userName=SessUser"
curlq "SETUP recordSession valid" -X POST "$B/recordSession?userId=stage3_sess&loginTime=2025-01-15T10:00:00&logoutTime=2025-01-15T11:30:00"
curlq "SETUP register stage3_month / monthly" -X POST "$B/register?userId=stage3_month&userName=M"
curlq "SETUP recordSession for monthlyActivity" -X POST "$B/recordSession?userId=stage3_month&loginTime=2025-01-10T08:00:00&logoutTime=2025-01-10T09:00:00"
curlq "SETUP register stage3_export" -X POST "$B/register?userId=stage3_export&userName=ExportUser"
curlq "SETUP register stage3_notify" -X POST "$B/register?userId=stage3_notify&userName=N"

# --- POST /register ---
curlq "POST /register — нет параметров" -X POST "$B/register"
curlq "POST /register — только userId" -X POST "$B/register?userId=onlyId"
curlq "POST /register — пустой userId (граничный случай)" -X POST "$B/register?userId=&userName=empty_id_user"
curlq "POST /register — дубликат userId" -X POST "$B/register?userId=stage3_base&userName=Other"
curlq "POST /register — userId со слэшами и точками" -X POST "$B/register?userId=..%2F..%2Fevil&userName=test"
curlq "POST /register — длинный userName (~800 символов)" -X POST "$B/register?userId=stage3_long&userName=$(python3 -c 'print("A"*800)')"

# --- POST /recordSession ---
curlq "POST /recordSession — нет параметров" -X POST "$B/recordSession"
curlq "POST /recordSession — битый ISO loginTime" -X POST "$B/recordSession?userId=stage3_sess&loginTime=not-a-date&logoutTime=2025-01-15T12:00:00"
curlq "POST /recordSession — неизвестный userId" -X POST "$B/recordSession?userId=no_such_user&loginTime=2025-01-15T10:00:00&logoutTime=2025-01-15T11:00:00"
curlq "POST /recordSession — logout раньше login (логика сервиса)" -X POST "$B/recordSession?userId=stage3_sess&loginTime=2025-01-20T12:00:00&logoutTime=2025-01-20T10:00:00"

# --- GET /totalActivity ---
curlq "GET /totalActivity — нет userId" "$B/totalActivity"
curlq "GET /totalActivity — несуществующий user" "$B/totalActivity?userId=ghost_user"
curlq "GET /totalActivity — норма" "$B/totalActivity?userId=stage3_sess"
curlq "GET /totalActivity — userId со спецсимволами в query" "$B/totalActivity?userId=stage3_%3Ctest%3E"

# --- GET /inactiveUsers ---
curlq "GET /inactiveUsers — нет days" "$B/inactiveUsers"
curlq "GET /inactiveUsers — days не число" "$B/inactiveUsers?days=abc"
curlq "GET /inactiveUsers — days отрицательное" "$B/inactiveUsers?days=-1"
curlq "GET /inactiveUsers — days=0" "$B/inactiveUsers?days=0"

# --- GET /monthlyActivity ---
curlq "GET /monthlyActivity — нет параметров" "$B/monthlyActivity"
curlq "GET /monthlyActivity — неверный month" "$B/monthlyActivity?userId=stage3_month&month=13-2025"
curlq "GET /monthlyActivity — пользователь без сессий (ожидание ошибки)" "$B/monthlyActivity?userId=stage3_base&month=2025-01"
curlq "GET /monthlyActivity — норма" "$B/monthlyActivity?userId=stage3_month&month=2025-01"

# --- GET /userProfile ---
curlq "GET /userProfile — нет userId" "$B/userProfile"
curlq "GET /userProfile — 404" "$B/userProfile?userId=nobody_here"
curlq "GET /userProfile — XSS user" "$B/userProfile?userId=stage3_xss"
curlq "GET /userProfile — кавычки в имени" "$B/userProfile?userId=stage3_quote"

# --- GET /exportReport ---
curlq "GET /exportReport — нет параметров" "$B/exportReport"
curlq "GET /exportReport — несуществующий user" "$B/exportReport?userId=ghost&filename=a.txt"
curlq "GET /exportReport — нормальное имя файла" "$B/exportReport?userId=stage3_export&filename=safe_report.txt"
curlq "GET /exportReport — path traversal (../)" "$B/exportReport?userId=stage3_export&filename=..%2F..%2Ftmp%2Flab4_escape.txt"

# --- POST /notify ---
curlq "POST /notify — нет параметров" -X POST "$B/notify"
curlq "POST /notify — несуществующий user" -X POST "$B/notify?userId=ghost&callbackUrl=http://127.0.0.1:1/"
curlq "POST /notify — невалидный URL" -X POST "$B/notify?userId=stage3_notify&callbackUrl=not-a-url"
curlq "POST /notify — connection refused (порт закрыт)" -X POST "$B/notify?userId=stage3_notify&callbackUrl=http://127.0.0.1:1/nope"
curlq "POST /notify — file:// схема" -X POST "$B/notify?userId=stage3_notify&callbackUrl=file:///etc/passwd"

echo "########## RATE — 20x GET /totalActivity подряд (коды) ##########"
for i in $(seq 1 20); do
code=$(curl -s -o /dev/null -w "%{http_code}" "$B/totalActivity?userId=stage3_sess")
echo -n "$code "
done
echo ""

echo "DONE"
27 changes: 27 additions & 0 deletions semgrep-output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@


┌─────────────┐
│ Scan Status │
└─────────────┘
Scanning 6 files tracked by git with 133 Code rules:

Language Rules Files Origin Rules
───────────────────────────── ───────────────────
java 112 6 Custom 131
<multilang> 1 6 Community 2



┌──────────────┐
│ Scan Summary │
└──────────────┘
✅ 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.
1 change: 1 addition & 0 deletions semgrep-report.sarif

Large diffs are not rendered by default.

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

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

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

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

/**
* Pentest: отсутствие аутентификации и проверки владельца ресурса (IDOR / broken access control, CWE-639).
* Любой клиент с знанием {@code userId} читает профиль и метрики жертвы.
*/
class BrokenAccessControlPentestTest {

private static final int TEST_PORT = 7781;
private static final String BASE = "http://localhost:" + TEST_PORT;
private static final String SECRET_NAME = "VictimSecretName_42";

private static Javalin app;
private static HttpClient http;

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

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

@Test
@DisplayName("[SECURITY] без токена: чужой userId открывает профиль (IDOR)")
void userProfileReadableWithoutAuth() throws Exception {
String enc = URLEncoder.encode(SECRET_NAME, StandardCharsets.UTF_8);
send("POST", BASE + "/register?userId=victim_idor&userName=" + enc);

HttpResponse<String> res = send("GET", BASE + "/userProfile?userId=victim_idor");

assertEquals(200, res.statusCode());
assertTrue(res.body().contains(SECRET_NAME),
"имя жертвы доступно любому, кто угадал/узнал userId — нет авторизации");
}

private static HttpResponse<String> send(String method, String uri) throws Exception {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(uri))
.method(method, HttpRequest.BodyPublishers.noBody())
.build();
return http.send(req, HttpResponse.BodyHandlers.ofString());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package ru.itmo.testing.lab4.pentest;

import io.javalin.Javalin;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
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.*;

/**
* Pentest: CWE-209 — генерация сообщений об ошибках с деталями исключений (утечка внутренней структуры валидации).
* После исправления: обобщённое сообщение пользователю, детали только в логах.
*/
class ErrorDisclosurePentestTest {

private static final int TEST_PORT = 7780;
private static final String BASE = "http://localhost:" + TEST_PORT;

private static Javalin app;
private static HttpClient http;

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

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

@Test
@DisplayName("[SECURITY] recordSession: тело 400 содержит текст исключения парсера даты")
void parseErrorExposesExceptionMessage() throws Exception {
send("POST", BASE + "/register?userId=pt_err&userName=E");
String q = "/recordSession?userId=pt_err&loginTime=not-a-date&logoutTime=2025-01-01T12:00:00";
HttpResponse<String> res = send("POST", BASE + q);

assertEquals(400, res.statusCode());
assertTrue(res.body().startsWith("Invalid data: "), "префикс раскрывает тип ошибки");
assertTrue(res.body().contains("could not be parsed"),
"в ответе клиенту — детали java.time (не должны утекать в проде)");
}

private static HttpResponse<String> send(String method, String uri) throws Exception {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(uri))
.method(method, HttpRequest.BodyPublishers.noBody())
.build();
return http.send(req, HttpResponse.BodyHandlers.ofString());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package ru.itmo.testing.lab4.pentest;

import io.javalin.Javalin;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
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 static org.junit.jupiter.api.Assertions.*;

/**
* Pentest: небезопасная обработка {@code callbackUrl} в {@code POST /notify} (CWE-918, чтение локальных ресурсов).
* После исправления: только http/https к allowlist-хостам, без {@code file:}.
*/
class NotifyUnsafeCallbackPentestTest {

private static final int TEST_PORT = 7779;
private static final String BASE = "http://localhost:" + TEST_PORT;

private static Javalin app;
private static HttpClient http;

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

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

@Test
@DisplayName("[SECURITY] notify: file:// отдаёт содержимое локального файла в ответе API")
void fileSchemeLeaksLocalFileContent() throws Exception {
Assumptions.assumeTrue(Files.isReadable(Path.of("/etc/passwd")), "нужен Unix с читаемым /etc/passwd");

send("POST", BASE + "/register?userId=pt_notify&userName=N");

String url = "file:///etc/passwd";
String q = "/notify?userId=pt_notify&callbackUrl=" + URLEncoder.encode(url, StandardCharsets.UTF_8);
HttpResponse<String> res = send("POST", BASE + q);

assertEquals(200, res.statusCode());
assertTrue(res.body().startsWith("Notification sent. Response:"),
"успешный ответ со встроенным телом «ответа» URL");
assertTrue(res.body().contains("root:"), "в теле API утекло содержимое /etc/passwd");
}

private static HttpResponse<String> send(String method, String uri) throws Exception {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(uri))
.method(method, HttpRequest.BodyPublishers.noBody())
.build();
return http.send(req, HttpResponse.BodyHandlers.ofString());
}
}
Loading