diff --git a/README.md b/README.md
index 18eee9a..d5177c7 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@
+[](https://classroom.github.com/a/NSTTkgmb)
# Лабораторная работа №4 — Анализ и тестирование безопасности веб-приложения
## Цель
@@ -42,12 +43,12 @@
| Актив | Тип | Ценность | Примечание |
|-------|-----|----------|------------|
-| Данные пользователей (userId, userName) | Данные | ? | |
-| Данные о сессиях (время входа/выхода) | Данные | ? | |
-| Файловая система сервера | Инфраструктура | ? | |
-| Внутренняя сеть / метаданные окружения | Инфраструктура | ? | |
+| Данные пользователей (userId, userName) | Данные | High | Имея userId можно смотреть чужие данные |
+| Данные о сессиях (время входа/выхода) | Данные | Low | Тамперинг аналитики входа/выхода |
+| Файловая система сервера | Инфраструктура | High | Path traversal |
+| Внутренняя сеть / метаданные окружения | Инфраструктура | High | SSRF moment |
-> **Вопрос для размышления:** какие из активов наиболее критичны и почему?
+> **Вопрос для размышления:** какие из активов наиболее критичны и почему? (внутренняя сеть)
---
@@ -57,12 +58,12 @@
| Категория угрозы | Расшифровка | Применимо к этому приложению? |
|------------------|------------------------|-------------------------------|
-| **S**poofing | Подмена идентификации | ? |
-| **T**ampering | Модификация данных | ? |
-| **R**epudiation | Отказ от авторства | ? |
-| **I**nformation Disclosure | Утечка данных | ? |
-| **D**enial of Service | Отказ в обслуживании | ? |
-| **E**levation of Privilege | Повышение привилегий | ? |
+| **S**poofing | Подмена идентификации | Да. Источник - внешний. Все эндпоинты. |
+| **T**ampering | Модификация данных | Да. Источник - внешний. recordSession, exportData |
+| **R**epudiation | Отказ от авторства | Да. Источник - внешний и внутренний. Везде где нет логов. |
+| **I**nformation Disclosure | Утечка данных | Да. Источник - внешний. totalActivity, userProfile |
+| **D**enial of Service | Отказ в обслуживании | Да. Источник - внешний. register, recordSession |
+| **E**levation of Privilege | Повышение привилегий | Да. Источник - внешний. notify, exportReport |
Для каждой применимой угрозы укажите:
- **Источник угрозы** (кто/что может её реализовать)
@@ -241,6 +242,128 @@ src/test/java/ru/itmo/testing/lab4/pentest/XssPentestTest.java
---
+#### 🔴 Finding #1 — Hardcoded World-Readable Path
+
+| Поле | Значение |
+|------|----------|
+| **Компонент** | `UserAnalyticsController.java` (константа класса) |
+| **Тип** | Hardcoded Sensitive Path |
+| **CWE** | [CWE-552](https://cwe.mitre.org/data/definitions/552.html) |
+| **CVSS v3.1** | `4.0 MEDIUM (AV:L/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N)` |
+| **Статус** | Confirmed |
+
+**Описание:** Путь экспорта отчётов жёстко задан как `/tmp/reports/` — общедоступная директория.
+
+**Влияние:** Локальный пользователь или процесс может прочитать или изменить отчёты, содержащие пользовательские данные.
+
+**Рекомендации по исправлению:** Заменить путь на защищённую директорию внутри домашнего каталога приложения, например `System.getProperty("user.home") + "/.app/reports"`.
+
+**Шаги воспроизведения:**
+1. Открыть UserAnalyticsController.java:39.
+2. REPORTS_BASE_DIR = "/tmp/reports/".
+3. Ожидаемый результат: защищённая директория.
+4. Фактический результат: общедоступная временная папка.
+
+
+---
+
+#### 🔴 Finding #2 — Information Disclosure via Exception Messages
+
+| Поле | Значение |
+|------|----------|
+| **Компонент** | `/recordSession`, `/monthlyActivity` |
+| **Тип** | Information Disclosure |
+| **CWE** | [CWE-209](https://cwe.mitre.org/data/definitions/209.html) |
+| **CVSS v3.1** | `5.3 MEDIUM (AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N)` |
+| **Статус** | Confirmed |
+
+**Описание:** Сообщения исключений напрямую возвращаются клиенту.
+
+**Влияние:** Атакующий получает информацию о внутренней структуре приложения, что облегчает дальнейшие атаки.
+
+**Рекомендации по исправлению:** Возвращать общее сообщение об ошибке, а детали логировать на сервере.
+
+**Шаги воспроизведения:**
+1. POST /recordSession с loginTime=invalid.
+2. Ответ содержит "Invalid data: Text 'invalid' could not be parsed...".
+3. Ожидаемый результат: "Invalid data format".
+4. Фактический результат: утечка деталей парсинга.
+
+
+---
+
+#### 🔴 Finding #3 — Path Traversal in Report Export
+
+| Поле | Значение |
+|------|----------|
+| **Компонент** | `/exportReport` |
+| **Тип** | Path Traversal |
+| **CWE** | [CWE-22](https://cwe.mitre.org/data/definitions/22.html) |
+| **CVSS v3.1** | `7.5 HIGH (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H)` |
+| **Статус** | Confirmed |
+
+**Описание:** `filename` конкатенируется без проверки, позволяя выход за пределы `/tmp/reports/`.
+
+**Влияние:** Атакующий может перезаписать системные файлы или записать вредоносные данные в произвольные директории.
+
+**Рекомендации по исправлению:** Использовать `Paths.get(baseDir).resolve(filename).normalize()` и проверять, что результат начинается с `baseDir`.
+
+**Шаги воспроизведения:**
+1. GET /exportReport?userId=x&filename=../../../etc/passwd
+2. Файл создаётся вне разрешённой директории.
+3. Ожидаемый результат: отклонение запроса.
+4. Фактический результат: запись в произвольную локацию.
+
+
+---
+
+#### 🔴 Finding #4 — Server-Side Request Forgery (SSRF)
+
+| Поле | Значение |
+|------|----------|
+| **Компонент** | `/notify` |
+| **Тип** | SSRF |
+| **CWE** | [CWE-918](https://cwe.mitre.org/data/definitions/918.html) |
+| **CVSS v3.1** | `8.6 HIGH (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N)` |
+| **Статус** | Confirmed |
+
+**Описание:** Сервер выполняет запросы по переданному URL без валидации.
+
+**Влияние:** Атакующий может сканировать внутреннюю сеть, получать метаданные облака или атаковать внутренние сервисы.
+
+**Рекомендации по исправлению:** Реализовать allowlist разрешённых доменов и блокировать внутренние IP-адреса (localhost, 169.254.169.254, 10.0.0.0/8 и т.д.).
+
+**Шаги воспроизведения:**
+1. POST /notify?userId=x&callbackUrl=http://169.254.169.254/latest/meta-data/
+2. Сервер обращается к внутреннему адресу.
+3. Ожидаемый результат: запросы только к разрешённым хостам.
+4. Фактический результат: доступ к внутренним ресурсам.
+
+
+---
+
+#### 🔴 Finding #5 — Reflected XSS
+
+| Поле | Значение |
+|------|----------|
+| **Компонент** | `/userProfile` |
+| **Тип** | Reflected XSS |
+| **CWE** | [CWE-79](https://cwe.mitre.org/data/definitions/79.html) |
+| **CVSS v3.1** | `6.1 MEDIUM (AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N)` |
+| **Статус** | Suspected |
+
+**Описание:** Имя пользователя вставляется в HTML без экранирования.
+
+**Влияние:** Атакующий может выполнить произвольный JavaScript в браузере жертвы, что приводит к краже сессий или фишингу.
+
+**Рекомендации по исправлению:** Использовать `StringEscapeUtils.escapeHtml4()` или шаблонизатор (JTE, Thymeleaf) с автоматическим экранированием.
+
+**Шаги воспроизведения:**
+1. Зарегистрировать пользователя с именем:
+2. Открыть /userProfile?userId=...
+3. Ожидаемый результат: отображение как текст.
+4. Фактический результат: выполнение скрипта.
+
## Полезные ресурсы
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
diff --git a/devshell.nix b/devshell.nix
new file mode 100644
index 0000000..61b95aa
--- /dev/null
+++ b/devshell.nix
@@ -0,0 +1,24 @@
+{ inputs, ... }:
+
+let
+ pkgs = import inputs.nixpkgs {
+ system = "x86_64-linux";
+ config.allowUnfree = true;
+ };
+in
+(pkgs.buildFHSEnv {
+ name = "java-devshell";
+
+ targetPkgs = p: with p; [
+ just
+ jdk
+ gradle
+ kotlin
+ jetbrains.idea
+ semgrep
+ ];
+
+ profile = ''
+ export JAVA_HOME="${pkgs.jdk.home}"
+ '';
+}).env
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..88dbbc9
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,78 @@
+{
+ "nodes": {
+ "blueprint": {
+ "inputs": {
+ "nixpkgs": "nixpkgs",
+ "systems": "systems"
+ },
+ "locked": {
+ "lastModified": 1771437256,
+ "narHash": "sha256-bLqwib+rtyBRRVBWhMuBXPCL/OThfokA+j6+uH7jDGU=",
+ "owner": "numtide",
+ "repo": "blueprint",
+ "rev": "06ee7190dc2620ea98af9eb225aa9627b68b0e33",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "blueprint",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1741513245,
+ "narHash": "sha256-7rTAMNTY1xoBwz0h7ZMtEcd8LELk9R5TzBPoHuhNSCk=",
+ "owner": "nixos",
+ "repo": "nixpkgs",
+ "rev": "e3e32b642a31e6714ec1b712de8c91a3352ce7e1",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nixos",
+ "ref": "nixos-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "nixpkgs_2": {
+ "locked": {
+ "lastModified": 1774701658,
+ "narHash": "sha256-CIS/4AMUSwUyC8X5g+5JsMRvIUL3YUfewe8K4VrbsSQ=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "b63fe7f000adcfa269967eeff72c64cafecbbebe",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixpkgs-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "blueprint": "blueprint",
+ "nixpkgs": "nixpkgs_2"
+ }
+ },
+ "systems": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..2877c46
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,10 @@
+{
+ description = "Nix devshell with Java, IntelliJ IDEA, Gradle, and Kotlin";
+
+ inputs = {
+ nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
+ blueprint.url = "github:numtide/blueprint";
+ };
+
+ outputs = inputs: inputs.blueprint { inherit inputs; };
+}
diff --git a/semgrep-report.sarif b/semgrep-report.sarif
new file mode 100644
index 0000000..470c213
--- /dev/null
+++ b/semgrep-report.sarif
@@ -0,0 +1 @@
+{"version":"2.1.0","runs":[{"invocations":[{"executionSuccessful":true,"toolExecutionNotifications":[{"descriptor":{"id":"Rule parse error"},"level":"error","message":{"text":"Rule parse error in rule javalin-missing-security-headers:\n Invalid pattern for Java: Stdlib.Parsing.Parse_error\n----- pattern -----\n$APP.after { ctx -> ctx.header(\"X-Content-Type-Options\", \"nosniff\") }\n\n----- end pattern -----\n"}}]}],"results":[{"fingerprints":{"matchBasedId/v1":"2b23e6021ba4136e713f27188a4f76a552ac45d72858a4fbee7ce1bf791977ffbb0edb6ff924b29b2031228565ef49aa4667be4d9d253a4c485d059af201149f_0"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"src/main/java/ru/itmo/testing/lab4/controller/UserAnalyticsController.java","uriBaseId":"%SRCROOT%"},"region":{"endColumn":67,"endLine":39,"snippet":{"text":" private static final String REPORTS_BASE_DIR = \"/tmp/reports/\";"},"startColumn":5,"startLine":39}}}],"message":{"text":"Hardcoded path to `/tmp/reports/` is world-readable on Unix systems.\nUse a dedicated directory outside the web root with proper permissions.\n"},"properties":{},"ruleId":"javalin-hardcoded-sensitive-path"},{"fingerprints":{"matchBasedId/v1":"f50c20715a60b67471c1ee82d0fa37768ded98bd934a76f2b7f10ce3c4afeeee2c060fc46bba446b92bc1513c724ffbd8fd34ea99911b594843892521e60dc03_0"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"src/main/java/ru/itmo/testing/lab4/controller/UserAnalyticsController.java","uriBaseId":"%SRCROOT%"},"region":{"endColumn":53,"endLine":46,"snippet":{"text":" String userId = ctx.queryParam(\"userId\");"},"startColumn":29,"startLine":46}}}],"message":{"text":"User input from `queryParam` is used without validation.\nConsider using Javalin's `Validator` (`.check(p -> !p.isEmpty())`).\n"},"properties":{},"ruleId":"javalin-missing-queryparam-validation"},{"fingerprints":{"matchBasedId/v1":"f50c20715a60b67471c1ee82d0fa37768ded98bd934a76f2b7f10ce3c4afeeee2c060fc46bba446b92bc1513c724ffbd8fd34ea99911b594843892521e60dc03_1"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"src/main/java/ru/itmo/testing/lab4/controller/UserAnalyticsController.java","uriBaseId":"%SRCROOT%"},"region":{"endColumn":57,"endLine":47,"snippet":{"text":" String userName = ctx.queryParam(\"userName\");"},"startColumn":31,"startLine":47}}}],"message":{"text":"User input from `queryParam` is used without validation.\nConsider using Javalin's `Validator` (`.check(p -> !p.isEmpty())`).\n"},"properties":{},"ruleId":"javalin-missing-queryparam-validation"},{"fingerprints":{"matchBasedId/v1":"f50c20715a60b67471c1ee82d0fa37768ded98bd934a76f2b7f10ce3c4afeeee2c060fc46bba446b92bc1513c724ffbd8fd34ea99911b594843892521e60dc03_2"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"src/main/java/ru/itmo/testing/lab4/controller/UserAnalyticsController.java","uriBaseId":"%SRCROOT%"},"region":{"endColumn":53,"endLine":57,"snippet":{"text":" String userId = ctx.queryParam(\"userId\");"},"startColumn":29,"startLine":57}}}],"message":{"text":"User input from `queryParam` is used without validation.\nConsider using Javalin's `Validator` (`.check(p -> !p.isEmpty())`).\n"},"properties":{},"ruleId":"javalin-missing-queryparam-validation"},{"fingerprints":{"matchBasedId/v1":"f50c20715a60b67471c1ee82d0fa37768ded98bd934a76f2b7f10ce3c4afeeee2c060fc46bba446b92bc1513c724ffbd8fd34ea99911b594843892521e60dc03_3"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"src/main/java/ru/itmo/testing/lab4/controller/UserAnalyticsController.java","uriBaseId":"%SRCROOT%"},"region":{"endColumn":59,"endLine":58,"snippet":{"text":" String loginTime = ctx.queryParam(\"loginTime\");"},"startColumn":32,"startLine":58}}}],"message":{"text":"User input from `queryParam` is used without validation.\nConsider using Javalin's `Validator` (`.check(p -> !p.isEmpty())`).\n"},"properties":{},"ruleId":"javalin-missing-queryparam-validation"},{"fingerprints":{"matchBasedId/v1":"f50c20715a60b67471c1ee82d0fa37768ded98bd934a76f2b7f10ce3c4afeeee2c060fc46bba446b92bc1513c724ffbd8fd34ea99911b594843892521e60dc03_4"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"src/main/java/ru/itmo/testing/lab4/controller/UserAnalyticsController.java","uriBaseId":"%SRCROOT%"},"region":{"endColumn":61,"endLine":59,"snippet":{"text":" String logoutTime = ctx.queryParam(\"logoutTime\");"},"startColumn":33,"startLine":59}}}],"message":{"text":"User input from `queryParam` is used without validation.\nConsider using Javalin's `Validator` (`.check(p -> !p.isEmpty())`).\n"},"properties":{},"ruleId":"javalin-missing-queryparam-validation"},{"fingerprints":{"matchBasedId/v1":"190fa48e0e30407b4cbef017263ccd92a6bb1607f2cae73dde7c7052e51173bf29d4074ac24933f7e01f30aba4e530c97c87286438441a403803cac4d2662200_0"},"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":"aa9aad78fe8ebbde33036a94c5ee03d121dbc02e9be93f12f99a436bddf70c866ad580363372c20ecd586edc8a9e99b2fc739c82d0ecdb716dd72bc820422497_0"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"src/main/java/ru/itmo/testing/lab4/controller/UserAnalyticsController.java","uriBaseId":"%SRCROOT%"},"region":{"endColumn":53,"endLine":79,"snippet":{"text":" String userId = ctx.queryParam(\"userId\");"},"startColumn":29,"startLine":79}}}],"message":{"text":"User input from `queryParam` is used without validation.\nConsider using Javalin's `Validator` (`.check(p -> !p.isEmpty())`).\n"},"properties":{},"ruleId":"javalin-missing-queryparam-validation"},{"fingerprints":{"matchBasedId/v1":"aa9aad78fe8ebbde33036a94c5ee03d121dbc02e9be93f12f99a436bddf70c866ad580363372c20ecd586edc8a9e99b2fc739c82d0ecdb716dd72bc820422497_1"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"src/main/java/ru/itmo/testing/lab4/controller/UserAnalyticsController.java","uriBaseId":"%SRCROOT%"},"region":{"endColumn":54,"endLine":89,"snippet":{"text":" String daysParam = ctx.queryParam(\"days\");"},"startColumn":32,"startLine":89}}}],"message":{"text":"User input from `queryParam` is used without validation.\nConsider using Javalin's `Validator` (`.check(p -> !p.isEmpty())`).\n"},"properties":{},"ruleId":"javalin-missing-queryparam-validation"},{"fingerprints":{"matchBasedId/v1":"aa9aad78fe8ebbde33036a94c5ee03d121dbc02e9be93f12f99a436bddf70c866ad580363372c20ecd586edc8a9e99b2fc739c82d0ecdb716dd72bc820422497_2"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"src/main/java/ru/itmo/testing/lab4/controller/UserAnalyticsController.java","uriBaseId":"%SRCROOT%"},"region":{"endColumn":53,"endLine":104,"snippet":{"text":" String userId = ctx.queryParam(\"userId\");"},"startColumn":29,"startLine":104}}}],"message":{"text":"User input from `queryParam` is used without validation.\nConsider using Javalin's `Validator` (`.check(p -> !p.isEmpty())`).\n"},"properties":{},"ruleId":"javalin-missing-queryparam-validation"},{"fingerprints":{"matchBasedId/v1":"aa9aad78fe8ebbde33036a94c5ee03d121dbc02e9be93f12f99a436bddf70c866ad580363372c20ecd586edc8a9e99b2fc739c82d0ecdb716dd72bc820422497_3"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"src/main/java/ru/itmo/testing/lab4/controller/UserAnalyticsController.java","uriBaseId":"%SRCROOT%"},"region":{"endColumn":56,"endLine":105,"snippet":{"text":" String monthParam = ctx.queryParam(\"month\");"},"startColumn":33,"startLine":105}}}],"message":{"text":"User input from `queryParam` is used without validation.\nConsider using Javalin's `Validator` (`.check(p -> !p.isEmpty())`).\n"},"properties":{},"ruleId":"javalin-missing-queryparam-validation"},{"fingerprints":{"matchBasedId/v1":"190fa48e0e30407b4cbef017263ccd92a6bb1607f2cae73dde7c7052e51173bf29d4074ac24933f7e01f30aba4e530c97c87286438441a403803cac4d2662200_1"},"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":"aa9aad78fe8ebbde33036a94c5ee03d121dbc02e9be93f12f99a436bddf70c866ad580363372c20ecd586edc8a9e99b2fc739c82d0ecdb716dd72bc820422497_4"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"src/main/java/ru/itmo/testing/lab4/controller/UserAnalyticsController.java","uriBaseId":"%SRCROOT%"},"region":{"endColumn":53,"endLine":123,"snippet":{"text":" String userId = ctx.queryParam(\"userId\");"},"startColumn":29,"startLine":123}}}],"message":{"text":"User input from `queryParam` is used without validation.\nConsider using Javalin's `Validator` (`.check(p -> !p.isEmpty())`).\n"},"properties":{},"ruleId":"javalin-missing-queryparam-validation"},{"fingerprints":{"matchBasedId/v1":"aa9aad78fe8ebbde33036a94c5ee03d121dbc02e9be93f12f99a436bddf70c866ad580363372c20ecd586edc8a9e99b2fc739c82d0ecdb716dd72bc820422497_5"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"src/main/java/ru/itmo/testing/lab4/controller/UserAnalyticsController.java","uriBaseId":"%SRCROOT%"},"region":{"endColumn":53,"endLine":143,"snippet":{"text":" String userId = ctx.queryParam(\"userId\");"},"startColumn":29,"startLine":143}}}],"message":{"text":"User input from `queryParam` is used without validation.\nConsider using Javalin's `Validator` (`.check(p -> !p.isEmpty())`).\n"},"properties":{},"ruleId":"javalin-missing-queryparam-validation"},{"fingerprints":{"matchBasedId/v1":"aa9aad78fe8ebbde33036a94c5ee03d121dbc02e9be93f12f99a436bddf70c866ad580363372c20ecd586edc8a9e99b2fc739c82d0ecdb716dd72bc820422497_6"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"src/main/java/ru/itmo/testing/lab4/controller/UserAnalyticsController.java","uriBaseId":"%SRCROOT%"},"region":{"endColumn":57,"endLine":144,"snippet":{"text":" String filename = ctx.queryParam(\"filename\");"},"startColumn":31,"startLine":144}}}],"message":{"text":"User input from `queryParam` is used without validation.\nConsider using Javalin's `Validator` (`.check(p -> !p.isEmpty())`).\n"},"properties":{},"ruleId":"javalin-missing-queryparam-validation"},{"fingerprints":{"matchBasedId/v1":"d3de2df1d52e57aa37474283b3036737899e18c1ba7eabbdad38e4b9276e726c5c1fd3d0b3628822840720767374299e3963ad776ce285245a793e7dca0b83b6_0"},"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":"f50c20715a60b67471c1ee82d0fa37768ded98bd934a76f2b7f10ce3c4afeeee2c060fc46bba446b92bc1513c724ffbd8fd34ea99911b594843892521e60dc03_5"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"src/main/java/ru/itmo/testing/lab4/controller/UserAnalyticsController.java","uriBaseId":"%SRCROOT%"},"region":{"endColumn":53,"endLine":168,"snippet":{"text":" String userId = ctx.queryParam(\"userId\");"},"startColumn":29,"startLine":168}}}],"message":{"text":"User input from `queryParam` is used without validation.\nConsider using Javalin's `Validator` (`.check(p -> !p.isEmpty())`).\n"},"properties":{},"ruleId":"javalin-missing-queryparam-validation"},{"fingerprints":{"matchBasedId/v1":"f50c20715a60b67471c1ee82d0fa37768ded98bd934a76f2b7f10ce3c4afeeee2c060fc46bba446b92bc1513c724ffbd8fd34ea99911b594843892521e60dc03_6"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"src/main/java/ru/itmo/testing/lab4/controller/UserAnalyticsController.java","uriBaseId":"%SRCROOT%"},"region":{"endColumn":63,"endLine":169,"snippet":{"text":" String callbackUrl = ctx.queryParam(\"callbackUrl\");"},"startColumn":34,"startLine":169}}}],"message":{"text":"User input from `queryParam` is used without validation.\nConsider using Javalin's `Validator` (`.check(p -> !p.isEmpty())`).\n"},"properties":{},"ruleId":"javalin-missing-queryparam-validation"},{"fingerprints":{"matchBasedId/v1":"c63c3a45885854883907352740e894a4404323c0da9ce91df84502edc2cbf2ee24f6da9653b32276af47591340c0d7a9a1f5e0c898d91c6658601f51e4c067c3_0"},"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":"Hardcoded path to `/tmp/reports/` is world-readable on Unix systems.\nUse a dedicated directory outside the web root with proper permissions.\n"},"help":{"markdown":"Hardcoded path to `/tmp/reports/` is world-readable on Unix systems.\nUse a dedicated directory outside the web root with proper permissions.\n","text":"Hardcoded path to `/tmp/reports/` is world-readable on Unix systems.\nUse a dedicated directory outside the web root with proper permissions.\n"},"id":"javalin-hardcoded-sensitive-path","name":"javalin-hardcoded-sensitive-path","properties":{"precision":"very-high","tags":["CWE-552","security"]},"shortDescription":{"text":"Semgrep Finding: javalin-hardcoded-sensitive-path"}},{"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":"warning"},"fullDescription":{"text":"CORS allows requests from any origin. Restrict to trusted origins using `allowHost()`.\n"},"help":{"markdown":"CORS allows requests from any origin. Restrict to trusted origins using `allowHost()`.\n","text":"CORS allows requests from any origin. Restrict to trusted origins using `allowHost()`.\n"},"id":"javalin-insecure-cors","name":"javalin-insecure-cors","properties":{"precision":"very-high","tags":["CWE-942","security"]},"shortDescription":{"text":"Semgrep Finding: javalin-insecure-cors"}},{"defaultConfiguration":{"level":"note"},"fullDescription":{"text":"User input from `queryParam` is used without validation.\nConsider using Javalin's `Validator` (`.check(p -> !p.isEmpty())`).\n"},"help":{"markdown":"User input from `queryParam` is used without validation.\nConsider using Javalin's `Validator` (`.check(p -> !p.isEmpty())`).\n","text":"User input from `queryParam` is used without validation.\nConsider using Javalin's `Validator` (`.check(p -> !p.isEmpty())`).\n"},"id":"javalin-missing-queryparam-validation","name":"javalin-missing-queryparam-validation","properties":{"precision":"very-high","tags":[]},"shortDescription":{"text":"Semgrep Finding: javalin-missing-queryparam-validation"}},{"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.152.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/semgrep.yaml b/semgrep.yaml
new file mode 100644
index 0000000..2a2bbd9
--- /dev/null
+++ b/semgrep.yaml
@@ -0,0 +1,143 @@
+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
+
+ - id: javalin-missing-security-headers
+ patterns:
+ - pattern: Javalin.create()
+ - pattern-not: |
+ $APP.after { ctx -> ctx.header("X-Content-Type-Options", "nosniff") }
+ - pattern-not: |
+ $APP.after { ctx -> ctx.header("X-Frame-Options", "DENY") }
+ message: |
+ Missing security headers. Add a global `after` handler to set:
+ - X-Content-Type-Options: nosniff
+ - X-Frame-Options: DENY
+ - Strict-Transport-Security (if using HTTPS)
+ languages:
+ - java
+ - kotlin
+ severity: INFO
+ metadata:
+ category: security
+ technology:
+ - javalin
+ references:
+ - https://owasp.org/www-project-secure-headers/
+
+ - id: javalin-hardcoded-sensitive-path
+ patterns:
+ - pattern: private static final String $VAR = "$PATH";
+ - metavariable-regex:
+ metavariable: $VAR
+ regex: (?i)(REPORTS_BASE_DIR|UPLOAD_DIR|TEMP_DIR|FILES_PATH)
+ - metavariable-regex:
+ metavariable: $PATH
+ regex: ^/tmp/.*
+ message: |
+ Hardcoded path to `/tmp/reports/` is world-readable on Unix systems.
+ Use a dedicated directory outside the web root with proper permissions.
+ languages:
+ - java
+ - kotlin
+ severity: WARNING
+ metadata:
+ cwe: "CWE-552"
+
+ - id: javalin-missing-queryparam-validation
+ patterns:
+ - pattern: $CTX.queryParam("...")
+ - pattern-not: |
+ $CTX.queryParam("...").check(...)
+ - pattern-inside: |
+ $APP.$METHOD("...", $CTX -> { ... })
+ message: |
+ User input from `queryParam` is used without validation.
+ Consider using Javalin's `Validator` (`.check(p -> !p.isEmpty())`).
+ languages:
+ - java
+ - kotlin
+ severity: INFO
+ metadata:
+ category: security
+ technology:
+ - javalin
+
+ - id: javalin-insecure-cors
+ patterns:
+ - pattern: $APP.enableCorsForAllOrigins()
+ message: |
+ CORS allows requests from any origin. Restrict to trusted origins using `allowHost()`.
+ languages:
+ - java
+ - kotlin
+ severity: WARNING
+ metadata:
+ cwe: "CWE-942"
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..317fbe8 100644
--- a/src/test/java/ru/itmo/testing/lab4/pentest/XssPentestTest.java
+++ b/src/test/java/ru/itmo/testing/lab4/pentest/XssPentestTest.java
@@ -4,47 +4,37 @@
import org.junit.jupiter.api.*;
import ru.itmo.testing.lab4.controller.UserAnalyticsController;
+import java.io.File;
+import java.io.IOException;
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 java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Comparator;
import static org.junit.jupiter.api.Assertions.*;
/**
* =============================================================================
- * ПРИМЕР PENTEST-ОТЧЁТА: CWE-79 — Reflected Cross-Site Scripting (XSS)
+ * ПОЛНЫЙ PENTEST-ОТЧЁТ ПО УЯЗВИМОСТЯМ JAVALIN ПРИЛОЖЕНИЯ
* =============================================================================
*
- * Компонент: GET /userProfile
- * 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
+ * Проверяемые уязвимости:
+ * 1. Hardcoded World-Readable Path (CWE-552)
+ * 2. Information Disclosure via Exception Messages (CWE-209)
+ * 3. Path Traversal in Report Export (CWE-22)
+ * 4. Server-Side Request Forgery / SSRF (CWE-918)
+ * 5. Missing Input Validation (CWE-20)
+ * 6. Reflected Cross-Site Scripting / XSS (CWE-79) — уже присутствует
*
- * ОПИСАНИЕ:
- * Эндпоинт /userProfile возвращает HTML-страницу с именем пользователя.
- * Значение userName берётся из хранилища (куда попало через /register)
- * и вставляется в разметку без экранирования.
- * Атакующий регистрирует пользователя с именем, содержащим JS-код,
- * после чего любой, кто откроет /userProfile?userId=..., выполнит этот скрипт.
- *
- * ВЕКТОР АТАКИ:
- * 1. POST /register?userId=evil&userName=
- * 2. GET /userProfile?userId=evil
- * → браузер жертвы исполняет произвольный JavaScript в контексте приложения.
- *
- * ВЛИЯНИЕ:
- * - Кража сессионных cookie / токенов
- * - Выполнение действий от имени жертвы (CSRF-like)
- * - Перенаправление на фишинговый сайт
- *
- * МЕРЫ ЗАЩИТЫ:
- * - Экранировать HTML-спецсимволы перед вставкой в разметку
- * (например, через Apache Commons Text: StringEscapeUtils.escapeHtml4())
- * - Установить заголовок Content-Security-Policy
- * - Никогда не использовать ctx.contentType("text/html") с пользовательским вводом напрямую
+ * Все тесты используют реальный экземпляр приложения на порту 7777.
* =============================================================================
*/
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@@ -53,35 +43,269 @@ class XssPentestTest {
private static final int TEST_PORT = 7777;
private static final String BASE_URL = "http://localhost:" + TEST_PORT;
- // Типичные XSS-пейлоады
- private static final String PAYLOAD_BASIC = "";
- private static final String PAYLOAD_IMG = "
";
- private static final String PAYLOAD_SVG = "