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/yatool.xml b/.idea/yatool.xml new file mode 100644 index 0000000..b1dc6f2 --- /dev/null +++ b/.idea/yatool.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file 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 — Анализ и тестирование безопасности веб-приложения ## Цель diff --git a/artifacts/01_asset_inventory.md b/artifacts/01_asset_inventory.md new file mode 100644 index 0000000..8690add --- /dev/null +++ b/artifacts/01_asset_inventory.md @@ -0,0 +1,17 @@ +# Этап 1 — Asset Inventory + +## Таблица активов + +| Актив | Тип | Ценность | Примечание | +|---|---|---|---| +| Данные пользователей (`userId`, `userName`) | Данные | Высокая | Используются в профиле и отчётах; компрометация ведёт к подмене профилей, XSS и ошибочным аналитическим выводам. | +| Данные о сессиях (время входа/выхода) | Данные | Высокая | Формируют метрики активности (`/totalActivity`, `/monthlyActivity`, `/inactiveUsers`); искажение влияет на бизнес-решения и целостность отчётности. | +| Файловая система сервера | Инфраструктура | Критическая | Через `/exportReport` формируется путь к файлу; при злоупотреблении возможна запись в нежелательные места с правами процесса. | +| Внутренняя сеть / метаданные окружения | Инфраструктура | Критическая | Через `/notify` выполняется запрос по произвольному `callbackUrl`; это открывает путь к SSRF и доступу к внутренним сервисам. | + +## Наиболее критичные активы и почему + +| Актив | Почему наиболее критичен | +|---|---| +| Файловая система сервера | Потенциальная запись произвольных файлов может повлиять на доступность/целостность приложения и создать условия для дальнейшей компрометации хоста. | +| Внутренняя сеть / метаданные окружения | SSRF позволяет атакующему использовать сервер как прокси к внутренним ресурсам, которые недоступны извне, и извлекать чувствительную информацию. | diff --git a/artifacts/02_stride_threat_model.md b/artifacts/02_stride_threat_model.md new file mode 100644 index 0000000..f15b377 --- /dev/null +++ b/artifacts/02_stride_threat_model.md @@ -0,0 +1,23 @@ +# Этап 2 — Threat Modeling (STRIDE) + +## Применимость STRIDE к приложению + +| Категория угрозы | Расшифровка | Применимо? | Короткое обоснование | +|---|---|---|---| +| **S**poofing | Подмена идентификации | Да | Нет аутентификации/авторизации; любой клиент может действовать от имени любого `userId`. | +| **T**ampering | Модификация данных | Да | Данные сессий и отчётов меняются через публичные эндпоинты без проверки прав. | +| **R**epudiation | Отказ от авторства | Да | Нет механизма неизменяемого аудита и привязки действий к субъекту. | +| **I**nformation Disclosure | Утечка данных | Да | Возможна SSRF через `callbackUrl` с возвратом ответа внешнему клиенту. | +| **D**enial of Service | Отказ в обслуживании | Да | Нет rate limit и лимитов входных данных; внешние вызовы могут блокировать ресурсы. | +| **E**levation of Privilege | Повышение привилегий | Да | Анонимный клиент получает влияние на внутренние ресурсы/ФС сервера через SSRF и запись файлов. | + +## Применимые угрозы: источник, поверхность атаки, ущерб + +| Категория | Источник угрозы | Поверхность атаки | Потенциальный ущерб | +|---|---|---|---| +| **S**poofing | Внешний неаутентифицированный клиент | `POST /recordSession?userId=...`, `GET /totalActivity?userId=...`, `GET /monthlyActivity?userId=...` | Подмена личности пользователя, недостоверная аналитика, доступ к действиям от чужого имени. | +| **T**ampering | Внешний атакующий | `POST /recordSession` (подмена времён), `GET /exportReport?filename=...` (манипуляция путём) | Порча данных активности, запись файлов в неожиданные директории, нарушение целостности системы. | +| **R**epudiation | Любой клиент API | Все write-операции: `/register`, `/recordSession`, `/notify`, `/exportReport` | Невозможность доказать, кто инициировал вредоносное действие; усложнение расследования инцидентов. | +| **I**nformation Disclosure | Внешний атакующий, использующий сервер как прокси | `POST /notify?callbackUrl=...` | Чтение внутренних HTTP-ресурсов (метаданные, служебные сервисы), утечка их ответа в теле API-ответа. | +| **D**enial of Service | Внешний атакующий (массовые/тяжёлые запросы) | Повторные вызовы `/notify` к медленным URL, большие/частые запросы к публичным эндпоинтам | Исчерпание потоков/сокетов, деградация производительности и недоступность API для легитимных пользователей. | +| **E**levation of Privilege | Внешний низкопривилегированный клиент | Комбинация `callbackUrl` в `/notify` и `filename` в `/exportReport` | Переход от внешнего доступа к воздействию на внутренние зоны доверия и ресурсы, доступные только серверу. | diff --git a/artifacts/03_manual_testing.md b/artifacts/03_manual_testing.md new file mode 100644 index 0000000..404e553 --- /dev/null +++ b/artifacts/03_manual_testing.md @@ -0,0 +1,87 @@ +# Этап 3 — Ручное тестирование + +## Условия выполнения + +| Поле | Значение | +|---|---| +| Дата тестирования | 2026-05-11 | +| База URL | `http://localhost:7000` | +| Инструмент | `curl` | +| Сырой лог | `artifacts/03_manual_testing_raw.log` | + +## `POST /register` + +| Кейc | Цель | curl-запрос | HTTP-код | Важный фрагмент ответа | Вывод | +|---|---|---|---|---|---| +| R8 | Позитив: регистрация нового пользователя | `curl -sS -X POST --get --data-urlencode "userId=fresh_user" --data-urlencode "userName=Fresh" "http://localhost:7000/register"` | 200 | `User registered: true` | Базовая регистрация работает. | +| R2 | Граница: повторная регистрация того же `userId` | `curl -sS -X POST --get --data-urlencode "userId=u_normal" --data-urlencode "userName=Alice2" "http://localhost:7000/register"` | 200 | `User registered: false` | Дубликат отклоняется, но возвращается 200 с флагом в теле. | +| R3 | Негатив: отсутствует `userName` | `curl -sS -X POST --get --data-urlencode "userId=missing_name" "http://localhost:7000/register"` | 400 | `Missing parameters` | Есть базовая валидация обязательных параметров. | +| R4 | Security: ввод XSS-пейлоада в `userName` | `curl -sS -X POST --get --data-urlencode "userId=xss_user" --data-urlencode "userName=" "http://localhost:7000/register"` | 200 | `User registered: true` | Ввод со скриптом сохраняется без фильтрации. | +| R5 | Security: спецсимволы `< > " ' / ..` в `userName` | `curl -sS -X POST --get --data-urlencode "userId=special_chars" --data-urlencode "userName=\"' / .." "http://localhost:7000/register"` | 200 | `User registered: true` | Спецсимволы принимаются и сохраняются. | + +## `POST /recordSession` + +| Кейc | Цель | curl-запрос | HTTP-код | Важный фрагмент ответа | Вывод | +|---|---|---|---|---|---| +| S1 | Позитив: корректные ISO-времена | `curl -sS -X POST --get --data-urlencode "userId=u_normal" --data-urlencode "loginTime=2026-05-10T10:00:00" --data-urlencode "logoutTime=2026-05-10T11:30:00" "http://localhost:7000/recordSession"` | 200 | `Session recorded` | Валидная сессия сохраняется. | +| S2 | Негатив: неверный формат `loginTime` | `curl -sS -X POST --get --data-urlencode "userId=u_normal" --data-urlencode "loginTime=bad-time" --data-urlencode "logoutTime=2026-05-10T11:30:00" "http://localhost:7000/recordSession"` | 400 | `Invalid data: Text 'bad-time' could not be parsed` | Формат даты/времени валидируется. | +| S3 | Негатив: отсутствует `logoutTime` | `curl -sS -X POST --get --data-urlencode "userId=u_normal" --data-urlencode "loginTime=2026-05-10T10:00:00" "http://localhost:7000/recordSession"` | 400 | `Missing parameters` | Обязательные параметры проверяются. | +| S4 | Негатив: несуществующий пользователь | `curl -sS -X POST --get --data-urlencode "userId=ghost" --data-urlencode "loginTime=2026-05-10T10:00:00" --data-urlencode "logoutTime=2026-05-10T11:30:00" "http://localhost:7000/recordSession"` | 404 | `User not found` | Для неизвестного `userId` запись отклоняется. | +| S6 | Security/граница: `logoutTime < loginTime` | `curl -sS -X POST --get --data-urlencode "userId=u_normal" --data-urlencode "loginTime=2026-05-10T12:00:00" --data-urlencode "logoutTime=2026-05-10T11:00:00" "http://localhost:7000/recordSession"` | 200 | `Session recorded` | Нелогичная сессия принимается, контроль порядка времени отсутствует. | + +## `GET /totalActivity` + +| Кейc | Цель | curl-запрос | HTTP-код | Важный фрагмент ответа | Вывод | +|---|---|---|---|---|---| +| T1 | Позитив: активность существующего пользователя | `curl -sS -G --data-urlencode "userId=u_normal" "http://localhost:7000/totalActivity"` | 200 | `Total activity: 90 minutes` | Расчёт работает для валидных данных. | +| T2 | Граница после tampering-ввода | `curl -sS -G --data-urlencode "userId=u_normal" "http://localhost:7000/totalActivity"` | 200 | `Total activity: 30 minutes` | Принятая отрицательная сессия уменьшила итог, что подтверждает проблему целостности. | +| T3 | Негатив: отсутствует `userId` | `curl -sS -G "http://localhost:7000/totalActivity"` | 400 | `Missing userId` | Есть базовая проверка параметра. | +| T4 | Граница: неизвестный `userId` | `curl -sS -G --data-urlencode "userId=ghost" "http://localhost:7000/totalActivity"` | 200 | `Total activity: 0 minutes` | Для несуществующего пользователя возвращается 0, а не ошибка. | + +## `GET /inactiveUsers` + +| Кейc | Цель | curl-запрос | HTTP-код | Важный фрагмент ответа | Вывод | +|---|---|---|---|---|---| +| I1 | Позитив: получить список при `days=30` | `curl -sS -G --data-urlencode "days=30" "http://localhost:7000/inactiveUsers"` | 500 | `It looks like you don't have an object mapper configured` | Возврат JSON падает из-за отсутствия object mapper. | +| I2 | Негатив: отсутствует `days` | `curl -sS -G "http://localhost:7000/inactiveUsers"` | 400 | `Missing days parameter` | Проверка обязательного параметра есть. | +| I3 | Негатив: нечисловой `days` | `curl -sS -G --data-urlencode "days=abc" "http://localhost:7000/inactiveUsers"` | 400 | `Invalid number format for days` | Формат числа валидируется. | +| I4 | Security/граница: отрицательный `days=-1` | `curl -sS -G --data-urlencode "days=-1" "http://localhost:7000/inactiveUsers"` | 500 | `It looks like you don't have an object mapper configured` | Ветка с JSON также падает 500; корректность обработки отрицательной границы не проверить из-за ошибки сериализации. | + +## `GET /monthlyActivity` + +| Кейc | Цель | curl-запрос | HTTP-код | Важный фрагмент ответа | Вывод | +|---|---|---|---|---|---| +| M1 | Позитив: метрика за валидный месяц | `curl -sS -G --data-urlencode "userId=u_normal" --data-urlencode "month=2026-05" "http://localhost:7000/monthlyActivity"` | 400 | `It looks like you don't have an object mapper configured` | Позитивный JSON-ответ недоступен из-за отсутствия object mapper. | +| M2 | Негатив: отсутствует `month` | `curl -sS -G --data-urlencode "userId=u_normal" "http://localhost:7000/monthlyActivity"` | 400 | `Missing parameters` | Проверка обязательных параметров есть. | +| M3 | Негатив: неверный формат месяца | `curl -sS -G --data-urlencode "userId=u_normal" --data-urlencode "month=2026/05" "http://localhost:7000/monthlyActivity"` | 400 | `Text '2026/05' could not be parsed at index 4` | Формат `yyyy-MM` валидируется. | +| M4 | Граница: пользователь без сессий | `curl -sS -G --data-urlencode "userId=no_session_user" --data-urlencode "month=2026-05" "http://localhost:7000/monthlyActivity"` | 400 | `Invalid data: No sessions found for user` | Для пользователя без сессий возвращается ошибка бизнес-логики. | + +## `GET /userProfile` + +| Кейc | Цель | curl-запрос | HTTP-код | Важный фрагмент ответа | Вывод | +|---|---|---|---|---|---| +| U1 | Позитив: профиль существующего пользователя | `curl -sS -G --data-urlencode "userId=u_normal" "http://localhost:7000/userProfile"` | 200 | `

Profile: Alice

` | HTML-профиль возвращается корректно. | +| U2 | Негатив: отсутствует `userId` | `curl -sS -G "http://localhost:7000/userProfile"` | 400 | `Missing userId` | Проверка обязательного параметра есть. | +| U3 | Негатив: неизвестный `userId` | `curl -sS -G --data-urlencode "userId=ghost" "http://localhost:7000/userProfile"` | 404 | `User not found` | Для неизвестного пользователя возвращается 404. | +| U4 | Security: XSS-пейлоад в HTML без экранирования | `curl -sS -G --data-urlencode "userId=xss_user" "http://localhost:7000/userProfile"` | 200 | `

Profile:

` | Подтверждён reflected/stored XSS-вектор. | +| U5 | Security: отражение `< > " ' / ..` | `curl -sS -G --data-urlencode "userId=special_chars" "http://localhost:7000/userProfile"` | 200 | `

Profile: "' / ..

` | HTML-спецсимволы отдаются без нейтрализации. | + +## `GET /exportReport` + +| Кейc | Цель | curl-запрос | HTTP-код | Важный фрагмент ответа | Вывод | +|---|---|---|---|---|---| +| E1 | Позитив: экспорт в обычный файл | `curl -sS -G --data-urlencode "userId=u_normal" --data-urlencode "filename=report.txt" "http://localhost:7000/exportReport"` | 200 | `Report saved to: /tmp/reports/report.txt` | Экспорт работает. | +| E2 | Негатив: отсутствует `filename` | `curl -sS -G --data-urlencode "userId=u_normal" "http://localhost:7000/exportReport"` | 400 | `Missing parameters` | Проверка обязательных параметров есть. | +| E3 | Негатив: неизвестный `userId` | `curl -sS -G --data-urlencode "userId=ghost" --data-urlencode "filename=ghost.txt" "http://localhost:7000/exportReport"` | 404 | `User not found` | Для неизвестного пользователя экспорт не выполняется. | +| E4 | Security: path traversal через `../` | `curl -sS -G --data-urlencode "userId=u_normal" --data-urlencode "filename=../escape.txt" "http://localhost:7000/exportReport"` | 200 | `Report saved to: /tmp/reports/../escape.txt` | Путь не нормализуется/не фильтруется, traversal-вектор подтверждён. | + +## `POST /notify` + +| Кейc | Цель | curl-запрос | HTTP-код | Важный фрагмент ответа | Вывод | +|---|---|---|---|---|---| +| N1 | Позитив: callback на локальный эндпоинт | `curl -sS -X POST --get --data-urlencode "userId=u_normal" --data-urlencode "callbackUrl=http://localhost:7000/totalActivity?userId=u_normal" "http://localhost:7000/notify"` | 200 | `Notification sent. Response: Total activity: 30 minutes` | Внешний запрос по переданному URL выполняется. | +| N2 | Негатив: отсутствует `callbackUrl` | `curl -sS -X POST --get --data-urlencode "userId=u_normal" "http://localhost:7000/notify"` | 400 | `Missing parameters` | Проверка обязательных параметров есть. | +| N3 | Негатив: неизвестный `userId` | `curl -sS -X POST --get --data-urlencode "userId=ghost" --data-urlencode "callbackUrl=http://localhost:7000/totalActivity?userId=u_normal" "http://localhost:7000/notify"` | 404 | `User not found` | Для неизвестного пользователя операция блокируется. | +| N4 | Негатив/security: невалидный URL | `curl -sS -X POST --get --data-urlencode "userId=u_normal" --data-urlencode "callbackUrl=not-a-url" "http://localhost:7000/notify"` | 500 | `Notification failed: no protocol: not-a-url` | Ошибка URL обрабатывается, но возвращается 500 с деталями исключения. | +| N5 | Security: SSRF к внутреннему endpoint (`/userProfile`) | `curl -sS -X POST --get --data-urlencode "userId=u_normal" --data-urlencode "callbackUrl=http://localhost:7000/userProfile?userId=xss_user" "http://localhost:7000/notify"` | 200 | `Notification sent. Response: ......` | Сервер ходит по произвольному URL и возвращает контент вызывающей стороне. | +| N6 | Security: чтение локального файла через `file://` | `curl -sS -X POST --get --data-urlencode "userId=u_normal" --data-urlencode "callbackUrl=file:///etc/hosts" "http://localhost:7000/notify"` | 200 | `Notification sent. Response: ... localhost ...` | Подтверждён доступ к локальным файлам через URL-схему `file://`. | diff --git a/artifacts/03_manual_testing_raw.log b/artifacts/03_manual_testing_raw.log new file mode 100644 index 0000000..e37295a --- /dev/null +++ b/artifacts/03_manual_testing_raw.log @@ -0,0 +1,300 @@ +=== CASE R1 === +GOAL: POST /register positive: normal registration +CMD: curl -sS -X POST --get --data-urlencode userId=u_normal --data-urlencode userName=Alice -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/register +CURL_EXIT_CODE: 0 +HTTP_CODE: 200 +RESPONSE_SNIPPET: User registered: false + +=== CASE R2 === +GOAL: POST /register boundary: duplicate userId +CMD: curl -sS -X POST --get --data-urlencode userId=u_normal --data-urlencode userName=Alice2 -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/register +CURL_EXIT_CODE: 0 +HTTP_CODE: 200 +RESPONSE_SNIPPET: User registered: false + +=== CASE R3 === +GOAL: POST /register negative: missing userName +CMD: curl -sS -X POST --get --data-urlencode userId=missing_name -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/register +CURL_EXIT_CODE: 0 +HTTP_CODE: 400 +RESPONSE_SNIPPET: Missing parameters + +=== CASE R4 === +GOAL: POST /register security: store XSS payload in userName +CMD: curl -sS -X POST --get --data-urlencode userId=xss_user --data-urlencode userName=\alert\(1\)\ -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/register +CURL_EXIT_CODE: 0 +HTTP_CODE: 200 +RESPONSE_SNIPPET: User registered: true + +=== CASE R5 === +GOAL: POST /register security chars: userName with < > " ' / .. +CMD: curl -sS -X POST --get --data-urlencode userId=special_chars --data-urlencode userName=\\"\'\ /\ ..\ -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/register +CURL_EXIT_CODE: 0 +HTTP_CODE: 200 +RESPONSE_SNIPPET: User registered: true + +=== CASE R6 === +GOAL: POST /register prep: register old_user +CMD: curl -sS -X POST --get --data-urlencode userId=old_user --data-urlencode userName=Old -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/register +CURL_EXIT_CODE: 0 +HTTP_CODE: 200 +RESPONSE_SNIPPET: User registered: true + +=== CASE R7 === +GOAL: POST /register prep: register no_session_user +CMD: curl -sS -X POST --get --data-urlencode userId=no_session_user --data-urlencode userName=NoSession -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/register +CURL_EXIT_CODE: 0 +HTTP_CODE: 200 +RESPONSE_SNIPPET: User registered: true + +=== CASE S1 === +GOAL: POST /recordSession positive: valid ISO timestamps +CMD: curl -sS -X POST --get --data-urlencode userId=u_normal --data-urlencode loginTime=2026-05-10T10:00:00 --data-urlencode logoutTime=2026-05-10T11:30:00 -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/recordSession +CURL_EXIT_CODE: 0 +HTTP_CODE: 200 +RESPONSE_SNIPPET: Session recorded + +=== CASE S2 === +GOAL: POST /recordSession negative: invalid datetime format +CMD: curl -sS -X POST --get --data-urlencode userId=u_normal --data-urlencode loginTime=bad-time --data-urlencode logoutTime=2026-05-10T11:30:00 -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/recordSession +CURL_EXIT_CODE: 0 +HTTP_CODE: 400 +RESPONSE_SNIPPET: Invalid data: Text 'bad-time' could not be parsed at index 0 + +=== CASE S3 === +GOAL: POST /recordSession negative: missing logoutTime +CMD: curl -sS -X POST --get --data-urlencode userId=u_normal --data-urlencode loginTime=2026-05-10T10:00:00 -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/recordSession +CURL_EXIT_CODE: 0 +HTTP_CODE: 400 +RESPONSE_SNIPPET: Missing parameters + +=== CASE S4 === +GOAL: POST /recordSession negative: unknown user +CMD: curl -sS -X POST --get --data-urlencode userId=ghost --data-urlencode loginTime=2026-05-10T10:00:00 --data-urlencode logoutTime=2026-05-10T11:30:00 -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/recordSession +CURL_EXIT_CODE: 0 +HTTP_CODE: 404 +RESPONSE_SNIPPET: User not found + +=== CASE S5 === +GOAL: POST /recordSession positive prep: old session for old_user +CMD: curl -sS -X POST --get --data-urlencode userId=old_user --data-urlencode loginTime=2020-01-01T10:00:00 --data-urlencode logoutTime=2020-01-01T12:00:00 -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/recordSession +CURL_EXIT_CODE: 0 +HTTP_CODE: 200 +RESPONSE_SNIPPET: Session recorded + +=== CASE T1 === +GOAL: GET /totalActivity positive: existing user +CMD: curl -sS -G --data-urlencode userId=u_normal -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/totalActivity +CURL_EXIT_CODE: 0 +HTTP_CODE: 200 +RESPONSE_SNIPPET: Total activity: 90 minutes + +=== CASE M1 === +GOAL: GET /monthlyActivity positive: valid month +CMD: curl -sS -G --data-urlencode userId=u_normal --data-urlencode month=2026-05 -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/monthlyActivity +CURL_EXIT_CODE: 0 +HTTP_CODE: 400 +RESPONSE_SNIPPET: Invalid data: It looks like you don't have an object mapper configured. The easiest way to fix this is to simply add the 'jackson-databind' dependency: pom.xml: com.fasterxml.jackson.core jackson-databind + com.fasterxml.jackson.core + jackson-dat + +=== CASE I2 === +GOAL: GET /inactiveUsers negative: missing days +CMD: curl -sS -G -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/inactiveUsers +CURL_EXIT_CODE: 0 +HTTP_CODE: 400 +RESPONSE_SNIPPET: Missing days parameter + +=== CASE I3 === +GOAL: GET /inactiveUsers negative: invalid days format +CMD: curl -sS -G --data-urlencode days=abc -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/inactiveUsers +CURL_EXIT_CODE: 0 +HTTP_CODE: 400 +RESPONSE_SNIPPET: Invalid number format for days + +=== CASE I4 === +GOAL: GET /inactiveUsers boundary/security: negative days +CMD: curl -sS -G --data-urlencode days=-1 -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/inactiveUsers +CURL_EXIT_CODE: 0 +HTTP_CODE: 500 +RESPONSE_SNIPPET: { "title": "It looks like you don't have an object mapper configured. +The easiest way to fix this is to simply add the 'jackson-databind' dependency: + +pom.xml: + + com.fasterxml.jackson.core + jackson-dat + +=== CASE M2 === +GOAL: GET /monthlyActivity negative: missing month +CMD: curl -sS -G --data-urlencode userId=u_normal -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/monthlyActivity +CURL_EXIT_CODE: 0 +HTTP_CODE: 400 +RESPONSE_SNIPPET: Missing parameters + +=== CASE M3 === +GOAL: GET /monthlyActivity negative: invalid month format +CMD: curl -sS -G --data-urlencode userId=u_normal --data-urlencode month=2026/05 -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/monthlyActivity +CURL_EXIT_CODE: 0 +HTTP_CODE: 400 +RESPONSE_SNIPPET: Invalid data: Text '2026/05' could not be parsed at index 4 + +=== CASE M4 === +GOAL: GET /monthlyActivity boundary: user without sessions +CMD: curl -sS -G --data-urlencode userId=no_session_user --data-urlencode month=2026-05 -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/monthlyActivity +CURL_EXIT_CODE: 0 +HTTP_CODE: 400 +RESPONSE_SNIPPET: Invalid data: No sessions found for user + +=== CASE U1 === +GOAL: GET /userProfile positive: known user +CMD: curl -sS -G --data-urlencode userId=u_normal -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/userProfile +CURL_EXIT_CODE: 0 +HTTP_CODE: 200 +RESPONSE_SNIPPET:

Profile: Alice

ID: u_normal

Total activity: 30 min

+ +=== CASE U2 === +GOAL: GET /userProfile negative: missing userId +CMD: curl -sS -G -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/userProfile +CURL_EXIT_CODE: 0 +HTTP_CODE: 400 +RESPONSE_SNIPPET: Missing userId + +=== CASE U3 === +GOAL: GET /userProfile negative: unknown user +CMD: curl -sS -G --data-urlencode userId=ghost -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/userProfile +CURL_EXIT_CODE: 0 +HTTP_CODE: 404 +RESPONSE_SNIPPET: User not found + +=== CASE U4 === +GOAL: GET /userProfile security: reflected raw

ID: xss_user

Total activity: 0 min

+ +=== CASE U5 === +GOAL: GET /userProfile security chars rendering: < > " ' / .. +CMD: curl -sS -G --data-urlencode userId=special_chars -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/userProfile +CURL_EXIT_CODE: 0 +HTTP_CODE: 200 +RESPONSE_SNIPPET:

Profile: "' / ..

ID: special_chars

Total activity: 0 min

+ +=== CASE E1 === +GOAL: GET /exportReport positive: normal filename +CMD: curl -sS -G --data-urlencode userId=u_normal --data-urlencode filename=report.txt -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/exportReport +CURL_EXIT_CODE: 0 +HTTP_CODE: 200 +RESPONSE_SNIPPET: Report saved to: /tmp/reports/report.txt + +=== CASE E2 === +GOAL: GET /exportReport negative: missing filename +CMD: curl -sS -G --data-urlencode userId=u_normal -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/exportReport +CURL_EXIT_CODE: 0 +HTTP_CODE: 400 +RESPONSE_SNIPPET: Missing parameters + +=== CASE E3 === +GOAL: GET /exportReport negative: unknown user +CMD: curl -sS -G --data-urlencode userId=ghost --data-urlencode filename=ghost.txt -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/exportReport +CURL_EXIT_CODE: 0 +HTTP_CODE: 404 +RESPONSE_SNIPPET: User not found + +=== CASE E4 === +GOAL: GET /exportReport security: path traversal with ../ in filename +CMD: curl -sS -G --data-urlencode userId=u_normal --data-urlencode filename=../escape.txt -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/exportReport +CURL_EXIT_CODE: 0 +HTTP_CODE: 200 +RESPONSE_SNIPPET: Report saved to: /tmp/reports/../escape.txt + +=== CASE N1 === +GOAL: POST /notify positive: callback to local endpoint +CMD: curl -sS -X POST --get --data-urlencode userId=u_normal --data-urlencode callbackUrl=http://localhost:7000/totalActivity\?userId=u_normal -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/notify +CURL_EXIT_CODE: 0 +HTTP_CODE: 200 +RESPONSE_SNIPPET: Notification sent. Response: Total activity: 30 minutes + +=== CASE N2 === +GOAL: POST /notify negative: missing callbackUrl +CMD: curl -sS -X POST --get --data-urlencode userId=u_normal -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/notify +CURL_EXIT_CODE: 0 +HTTP_CODE: 400 +RESPONSE_SNIPPET: Missing parameters + +=== CASE N3 === +GOAL: POST /notify negative: unknown user +CMD: curl -sS -X POST --get --data-urlencode userId=ghost --data-urlencode callbackUrl=http://localhost:7000/totalActivity\?userId=u_normal -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/notify +CURL_EXIT_CODE: 0 +HTTP_CODE: 404 +RESPONSE_SNIPPET: User not found + +=== CASE N4 === +GOAL: POST /notify negative: invalid URL format +CMD: curl -sS -X POST --get --data-urlencode userId=u_normal --data-urlencode callbackUrl=not-a-url -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/notify +CURL_EXIT_CODE: 0 +HTTP_CODE: 500 +RESPONSE_SNIPPET: Notification failed: no protocol: not-a-url + +=== CASE N5 === +GOAL: POST /notify security: SSRF to /userProfile with XSS-containing HTML +CMD: curl -sS -X POST --get --data-urlencode userId=u_normal --data-urlencode callbackUrl=http://localhost:7000/userProfile\?userId=xss_user -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/notify +CURL_EXIT_CODE: 0 +HTTP_CODE: 200 +RESPONSE_SNIPPET: Notification sent. Response:

Profile:

ID: xss_user

Total activity: 0 min

+ +=== CASE N6 === +GOAL: POST /notify security: URL parameter as local file scheme +CMD: curl -sS -X POST --get --data-urlencode userId=u_normal --data-urlencode callbackUrl=file:///etc/hosts -w \nHTTP_CODE:%\{http_code\} http://localhost:7000/notify +CURL_EXIT_CODE: 0 +HTTP_CODE: 200 +RESPONSE_SNIPPET: Notification sent. Response: ## # Host Database # # localhost is used to configure the loopback interface # when the system is booting. Do not change this entry. ## 127.0.0.1 localhost 255.255.255.255 broadcasthost ::1 localhost + +=== CASE R8 === +GOAL: POST /register positive: fresh normal registration +CMD: curl -sS -X POST --get --data-urlencode userId=fresh_user --data-urlencode userName=Fresh -w +HTTP_CODE:%{http_code} http://localhost:7000/register +CURL_EXIT_CODE: 0 +HTTP_CODE: 200 +RESPONSE_SNIPPET: User registered: true + diff --git a/artifacts/04_semgrep_analysis.md b/artifacts/04_semgrep_analysis.md new file mode 100644 index 0000000..12ee471 --- /dev/null +++ b/artifacts/04_semgrep_analysis.md @@ -0,0 +1,29 @@ +# Этап 4 — Статический анализ Semgrep + +## Выполненные команды + +```bash +semgrep ci --code +semgrep ci --code --sarif --sarif-output semgrep-report.sarif +``` + +## Сводка запуска + +| Конфиг | Rules run | Targets scanned | Findings | Статус запуска | +|---|---:|---:|---:|---| +| `semgrep ci --code` | 2884 | 27 | 0 | Успешно | +| `semgrep ci --code --sarif --sarif-output semgrep-report.sarif` | 2884 | 27 | 0 | Успешно, SARIF сохранён | + +## Findings + +| ruleId | level | file:line | описание | CWE | статус | обоснование | +|---|---|---|---|---|---|---| +| — | — | — | Findings отсутствуют (`results: []`) | — | False Positive | Ложноположительных срабатываний нет, так как Semgrep не вернул ни одного finding. | + +## Примечания по SARIF + +| Поле | Значение | +|---|---| +| `runs` | 1 | +| `runs[0].results` | 0 элементов | +| `runs[0].invocations[0].toolExecutionNotifications` | 1 warning (`Syntax error at line gradlew:72`) | diff --git a/artifacts/05_pentest_report.md b/artifacts/05_pentest_report.md new file mode 100644 index 0000000..7c0a114 --- /dev/null +++ b/artifacts/05_pentest_report.md @@ -0,0 +1,257 @@ +#### 🔴 Finding #1 — Reflected/Stored XSS в профиле пользователя + +| Поле | Значение | +|------|----------| +| **Компонент** | `GET /userProfile` (`UserAnalyticsController`) | +| **Тип** | Reflected/Stored Cross-Site Scripting (XSS) | +| **CWE** | [CWE-79](https://cwe.mitre.org/data/definitions/79.html) — Improper Neutralization of Input During Web Page Generation | +| **CVSS v3.1** | `6.1 MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N)` | +| **Статус** | Confirmed | + +**Код с уязвимостью:** +[UserAnalyticsController.java#L134](../src/main/java/ru/itmo/testing/lab4/controller/UserAnalyticsController.java), [UserAnalyticsController.java#L135](../src/main/java/ru/itmo/testing/lab4/controller/UserAnalyticsController.java), [UserAnalyticsController.java#L139](../src/main/java/ru/itmo/testing/lab4/controller/UserAnalyticsController.java) + +**Описание:** +> Значение `userName`, полученное от пользователя через `/register`, вставляется в HTML профиля без экранирования. Браузер интерпретирует внедренные теги/скрипт как исполняемый код. + +**Шаги воспроизведения:** +```text +1. POST /register?userId=xss_user&userName= +2. GET /userProfile?userId=xss_user +3. Ожидаемый результат: спецсимволы экранированы (например, <script>), скрипт не исполняется. + Фактический результат: в ответе присутствует сырой . +``` + +**Влияние:** +> Выполнение произвольного JavaScript в браузере жертвы, кража данных сессии/токенов, подмена контента страницы. + +**Рекомендации по исправлению:** +> Экранировать пользовательские данные перед вставкой в HTML (например, HTML escaping), использовать шаблонизатор с auto-escaping, добавить защитный CSP. + +**Security Test Case:** +```java +@Test +@DisplayName("[SECURITY] userProfile must escape user-controlled HTML") +void userProfileEscapesHtml() { + // Arrange: register user with