diff --git a/README.md b/README.md index 18eee9a..d5177c7 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 — Анализ и тестирование безопасности веб-приложения ## Цель @@ -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 = ""; + // XSS payloads (already defined) + private static final String PAYLOAD_BASIC = ""; + private static final String PAYLOAD_IMG = ""; + private static final String PAYLOAD_SVG = ""; private static Javalin app; private static HttpClient http; + private static final DateTimeFormatter ISO_FORMAT = DateTimeFormatter.ISO_LOCAL_DATE_TIME; @BeforeAll static void startServer() { app = UserAnalyticsController.createApp(); app.start(TEST_PORT); http = HttpClient.newHttpClient(); + + // Предварительная регистрация тестового пользователя для всех тестов + try { + send("POST", "/register?userId=test-user&userName=TestUser"); + LocalDateTime now = LocalDateTime.now(); + String login = now.minusHours(2).format(ISO_FORMAT); + String logout = now.minusHours(1).format(ISO_FORMAT); + send("POST", "/recordSession?userId=test-user&loginTime=" + enc(login) + "&logoutTime=" + enc(logout)); + } catch (Exception ignored) {} } @AfterAll static void stopServer() { app.stop(); + // Clean up test report files + try { + Files.walk(Paths.get("/tmp/reports/")) + .sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } catch (IOException ignored) {} } - // ------------------------------------------------------------------------- - // RECONNAISSANCE: убеждаемся, что эндпоинт вообще отдаёт HTML - // ------------------------------------------------------------------------- - + // ================================================================================================= + // Finding #1: Hardcoded World-Readable Path (CWE-552) + // ================================================================================================= @Test @Order(1) + @DisplayName("[CWE-552] Файлы отчётов создаются в общедоступной директории /tmp/reports/") + void testWorldReadablePathExposure() throws Exception { + // Убедимся, что приложение использует /tmp/reports/ + Path reportDir = Paths.get("/tmp/reports/"); + if (!Files.exists(reportDir)) { + Files.createDirectories(reportDir); + } + + String filename = "pentest-report-" + System.currentTimeMillis() + ".txt"; + HttpResponse response = send("GET", "/exportReport?userId=test-user&filename=" + enc(filename)); + + assertEquals(200, response.statusCode(), "Экспорт должен выполниться успешно"); + + Path reportFile = reportDir.resolve(filename); + assertTrue(Files.exists(reportFile), "Файл создан в /tmp/reports/ — уязвимость подтверждена"); + + // Проверяем права доступа (только для Unix) + if (!System.getProperty("os.name").toLowerCase().contains("win")) { + Path tmpDir = Paths.get("/tmp"); + assertTrue(Files.isWritable(tmpDir), "/tmp доступен на запись всем — другие пользователи могут читать отчёты"); + } + + // Удаляем тестовый файл + Files.deleteIfExists(reportFile); + } + + // ================================================================================================= + // Finding #2: Information Disclosure via Exception Messages (CWE-209) + // ================================================================================================= + @Test + @Order(2) + @DisplayName("[CWE-209] Сообщения исключений раскрывают внутренние детали парсинга") + void testExceptionMessageDisclosure() throws Exception { + // Отправляем заведомо некорректную дату + String url = "/recordSession?userId=test-user&loginTime=invalid-date&logoutTime=2024-01-01T10:00:00"; + + HttpResponse response = send("POST", url); + + assertEquals(400, response.statusCode()); + String body = response.body(); + + // Уязвимость: тело ответа содержит технические детали исключения + assertTrue(body.contains("Invalid data:"), "Ответ должен содержать сообщение об ошибке"); + assertTrue(body.contains("could not be parsed") || body.contains("ParseException") || body.contains("DateTimeParseException"), + "FAIL (ожидаемое поведение): исключение раскрывает детали парсинга. " + + "Должно быть: \"Invalid date format\" без технических подробностей."); + } + + @Test + @Order(3) + @DisplayName("[CWE-209] Сообщения исключений в /monthlyActivity также раскрывают детали") + void testExceptionDisclosureInMonthlyActivity() throws Exception { + String url = "/monthlyActivity?userId=test-user&month=invalid-month"; + + HttpResponse response = send("GET", url); + + assertEquals(400, response.statusCode()); + String body = response.body(); + + assertTrue(body.contains("Invalid data:")); + assertTrue(body.toLowerCase().contains("parse") || body.toLowerCase().contains("exception"), + "Технические детали исключения присутствуют в ответе"); + } + + // ================================================================================================= + // Finding #3: Path Traversal in Report Export (CWE-22) + // ================================================================================================= + @Test + @Order(4) + @DisplayName("[CWE-22] Path Traversal позволяет записать файл за пределами /tmp/reports/") + void testPathTraversalOutsideBaseDir() throws Exception { + String maliciousFilename = "../../../tmp/evil-pentest.txt"; + String url = "/exportReport?userId=test-user&filename=" + enc(maliciousFilename); + + HttpResponse response = send("GET", url); + + // Уязвимый код всё равно возвращает 200 + assertEquals(200, response.statusCode()); + + Path escapedFile = Paths.get("/tmp/evil-pentest.txt"); + boolean fileOutsideBase = Files.exists(escapedFile); + + if (fileOutsideBase) { + Files.deleteIfExists(escapedFile); + } + + assertTrue(fileOutsideBase, + "FAIL (ожидаемое поведение): файл создан за пределами /tmp/reports/ — Path Traversal работает! " + + "Должен быть отклонён или санитизирован."); + } + + @Test + @Order(5) + @DisplayName("[CWE-22] Path Traversal с попыткой перезаписи системного файла") + void testPathTraversalToSystemFile() throws Exception { + // Попытка записи в /etc/passwd (требуются права, но демонстрирует отсутствие валидации) + String maliciousFilename = "../../../etc/pentest-attempt.txt"; + String url = "/exportReport?userId=test-user&filename=" + enc(maliciousFilename); + + HttpResponse response = send("GET", url); + + // Даже если запись не удалась из-за прав, приложение не должно разрешать такой путь + // Но уязвимый код попытается создать файл и вернёт 500 или 200. + // Мы проверяем, что ответ не 400 (нет валидации пути). + assertNotEquals(400, response.statusCode(), + "Приложение должно отклонить путь, выходящий за базовую директорию. " + + "Сейчас запрос принят — уязвимость подтверждена."); + } + + // ================================================================================================= + // Finding #4: Server-Side Request Forgery / SSRF (CWE-918) + // ================================================================================================= + @Test + @Order(6) + @DisplayName("[CWE-918] SSRF позволяет обратиться к облачному метадата-сервису (169.254.169.254)") + void testSSRFToCloudMetadata() throws Exception { + String maliciousUrl = "http://169.254.169.254/latest/meta-data/"; + String url = "/notify?userId=test-user&callbackUrl=" + enc(maliciousUrl); + + HttpResponse response = send("POST", url); + + // Уязвимый код принимает любой URL и пытается соединиться + // В зависимости от окружения ответ может быть 500 (connection timeout) или 200, + // но не 400/403 (отклонение из-за валидации). + assertNotEquals(400, response.statusCode(), "Запрос с внутренним IP должен быть отклонён"); + assertNotEquals(403, response.statusCode(), "Запрос с внутренним IP должен быть отклонён"); + + // Если в ответе есть признаки того, что соединение было attempted, это подтверждает SSRF + String body = response.body(); + if (body.contains("Connection refused") || body.contains("timeout") || body.contains("Notification failed")) { + // Это ожидаемо при отсутствии реального сервиса, но главное — приложение пыталось соединиться + assertTrue(true, "SSRF подтверждён: приложение пыталось установить соединение с внутренним адресом"); + } + } + + @Test + @Order(7) + @DisplayName("[CWE-918] SSRF позволяет обратиться к localhost") + void testSSRFToLocalhost() throws Exception { + String maliciousUrl = "http://localhost:8080/admin"; + String url = "/notify?userId=test-user&callbackUrl=" + enc(maliciousUrl); + + HttpResponse response = send("POST", url); + + assertNotEquals(400, response.statusCode()); + assertNotEquals(403, response.statusCode()); + // Приложение пытается соединиться с localhost — SSRF подтверждён + } + + @Test + @Order(8) + @DisplayName("[CWE-918] SSRF с использованием file:// схемы (запрещённой)") + void testSSRFWithFileProtocol() throws Exception { + String maliciousUrl = "file:///etc/passwd"; + String url = "/notify?userId=test-user&callbackUrl=" + enc(maliciousUrl); + + HttpResponse response = send("POST", url); + + // Уязвимый код может выбросить исключение MalformedURLException или UnknownServiceException + // Но важно, что нет валидации схемы URL. + // Мы проверяем, что ответ не 400 (не отклонён). + assertNotEquals(400, response.statusCode()); + } + + // ================================================================================================= + // Finding #5: Missing Input Validation (CWE-20) + // ================================================================================================= + @Test + @Order(9) + @DisplayName("[CWE-20] Пустой параметр userId принимается как валидный (нет проверки на empty)") + void testMissingValidationEmptyUserId() throws Exception { + // Case 1: Parameter completely missing → 400 (correct) + HttpResponse response = send("GET", "/totalActivity"); + assertEquals(400, response.statusCode(), "Отсутствие параметра должно возвращать 400"); + + // Case 2: Parameter present but empty → 200 (VULNERABILITY) + response = send("GET", "/totalActivity?userId="); + assertEquals(200, response.statusCode(), + "FAIL: пустой userId принят как валидный. Должен возвращать 400, так как пустая строка не является допустимым идентификатором."); + assertTrue(response.body().contains("Total activity: 0 minutes"), + "Приложение обработало пустой userId как существующего пользователя с 0 активностью"); + } + + @Test + @Order(10) + @DisplayName("[CWE-20] Специальные символы в параметрах не фильтруются") + void testMissingValidationSpecialCharacters() throws Exception { + String maliciousUserId = "user'; DROP TABLE users--"; + String url = "/totalActivity?userId=" + enc(maliciousUserId); + HttpResponse response = send("GET", url); + + // Нет SQL, поэтому просто возвращается 0 (пользователь не найден) + assertEquals(200, response.statusCode()); + assertTrue(response.body().contains("Total activity: 0 minutes")); + + // В реальном приложении с БД это могло бы быть SQL-инъекцией. + // Отсутствие валидации позволяет передавать произвольные строки. + } + + @Test + @Order(11) + @DisplayName("[CWE-20] Очень длинные значения параметров не ограничиваются") + void testMissingValidationLongInput() throws Exception { + String longName = "A".repeat(2000); + // Регистрация пользователя с очень длинным именем + HttpResponse response = send("POST", "/register?userId=longuser&userName=" + enc(longName)); + assertEquals(200, response.statusCode()); + + // Проверяем, что профиль отображается (может вызвать проблемы с производительностью или памятью) + response = send("GET", "/userProfile?userId=longuser"); + assertEquals(200, response.statusCode()); + // Уязвимость: нет ограничения на длину ввода + } + + // ================================================================================================= + // Оригинальные XSS тесты + // ================================================================================================= + @Test + @Order(12) @DisplayName("[RECON] /userProfile возвращает text/html") void profileEndpointReturnsHtml() throws Exception { - // Сначала регистрируем обычного пользователя send("POST", "/register?userId=recon_user&userName=Alice"); HttpResponse response = send("GET", "/userProfile?userId=recon_user"); @@ -92,12 +316,8 @@ void profileEndpointReturnsHtml() throws Exception { "Ответ должен быть text/html — браузер будет рендерить его как разметку"); } - // ------------------------------------------------------------------------- - // EXPLOIT: базовый пейлоад отражается без экранирования") void imgOnerrorPayloadReflected() throws Exception { send("POST", "/register?userId=xss2&userName=" + enc(PAYLOAD_IMG)); @@ -133,7 +347,7 @@ void imgOnerrorPayloadReflected() throws Exception { } @Test - @Order(4) + @Order(15) @DisplayName("[EXPLOIT] пейлоад отражается без экранирования") void svgOnloadPayloadReflected() throws Exception { send("POST", "/register?userId=xss3&userName=" + enc(PAYLOAD_SVG)); @@ -144,12 +358,8 @@ void svgOnloadPayloadReflected() throws Exception { "SVG-вектор работает в современных браузерах даже без +### Сначала зарегистрируем пользователя с этим пейлоадом +POST {{baseUrl}}/register?userId=xss_img&userName= + +> {% + client.assert(response.status === 200); +%} + +### + +GET {{baseUrl}}/userProfile?userId=xss_img + +> {% + client.test("img onerror vector works", function() { + client.assert(response.body.includes("")); + }); +%} + +### 6.4 XSS‑пейлоад через +POST {{baseUrl}}/register?userId=xss_svg&userName= + +> {% + client.assert(response.status === 200); +%} + +### + +GET {{baseUrl}}/userProfile?userId=xss_svg + +> {% + client.test("svg onload vector works", function() { + client.assert(response.body.includes("")); + }); +%} + +### ---------------------------------------------------------------------------- +### 7. EXPORTREPORT – Экспорт отчёта (Path Traversal – CWE‑22) +### ---------------------------------------------------------------------------- + +### 7.1 Нормальный экспорт +GET {{baseUrl}}/exportReport?userId=alice&filename=report.txt + +> {% + client.test("Report saved", function() { + client.assert(response.body.includes("Report saved to: /tmp/reports/report.txt")); + }); +%} + +### 7.2 Path Traversal – выход за пределы директории +GET {{baseUrl}}/exportReport?userId=alice&filename=../../../tmp/evil.txt + +> {% + client.test("Path traversal attempt accepted", function() { + client.assert(response.status === 200, + "VULN (CWE-22): Path traversal payload accepted – file written outside base dir"); + client.assert(response.body.includes("/tmp/evil.txt")); + }); +%} + +### 7.3 Path Traversal – попытка записи в /etc +GET {{baseUrl}}/exportReport?userId=alice&filename=../../../etc/test.txt + +> {% + client.test("Attempt to write to /etc", function() { + // Вероятно 500 из-за прав доступа, но запрос принят + client.assert(response.status !== 400, + "VULN: No validation to reject paths escaping base directory"); + }); +%} + +### 7.4 Имя файла с нулевым байтом (NULL byte injection) +GET {{baseUrl}}/exportReport?userId=alice&filename=../../../../etc/passwd%00.txt + +> {% + client.test("NULL byte injection attempt", function() { + client.log("Response: " + response.body); + }); +%} + +### ---------------------------------------------------------------------------- +### 8. NOTIFY – Уведомление по вебхуку (SSRF – CWE‑918) +### ---------------------------------------------------------------------------- + +### 8.1 Нормальный вебхук (внешний URL) +POST {{baseUrl}}/notify?userId=alice&callbackUrl=http://httpbin.org/anything + +> {% + client.test("Notification sent to external URL", function() { + client.assert(response.status === 200); + client.assert(response.body.includes("Notification sent")); + }); +%} + +### 8.2 SSRF – обращение к облачному метаданным +POST {{baseUrl}}/notify?userId=alice&callbackUrl=http://169.254.169.254/latest/meta-data/ + +> {% + client.test("SSRF to AWS metadata", function() { + // В локальной среде будет таймаут/отказ соединения, но запрос принят + client.assert(response.status !== 400 && response.status !== 403, + "VULN (CWE-918): Request to internal IP accepted without validation"); + client.log("Response: " + response.body); + }); +%} + +### 8.3 SSRF – localhost +POST {{baseUrl}}/notify?userId=alice&callbackUrl=http://localhost:7000/admin + +> {% + client.test("SSRF to localhost", function() { + client.assert(response.status !== 400 && response.status !== 403, + "VULN: Access to localhost not blocked"); + }); +%} + +### 8.4 SSRF – file:// протокол +POST {{baseUrl}}/notify?userId=alice&callbackUrl=file:///etc/passwd + +> {% + client.test("SSRF with file:// protocol", function() { + client.assert(response.status !== 400, + "VULN: No restriction on URL scheme"); + client.log("Response: " + response.body); + }); +%} + +### 8.5 SSRF – DNS rebinding проверка +POST {{baseUrl}}/notify?userId=alice&callbackUrl=http://127.0.0.1.nip.io:7000/ + +> {% + client.test("SSRF via DNS rebinding domain", function() { + client.assert(response.status !== 400, + "VULN: Domain resolves to internal IP but not blocked"); + }); +%} + +### ---------------------------------------------------------------------------- +### 9. Дополнительные проверки (Security Headers, CORS, Rate Limiting) +### ---------------------------------------------------------------------------- + +### 9.1 Проверка заголовков безопасности +GET {{baseUrl}}/userProfile?userId=alice + +> {% + client.test("Security headers are missing", function() { + client.assert(!response.headers["X-Content-Type-Options"], + "Missing X-Content-Type-Options"); + client.assert(!response.headers["X-Frame-Options"], + "Missing X-Frame-Options"); + client.assert(!response.headers["Content-Security-Policy"], + "Missing CSP"); + client.log("VULN: No security headers present"); + }); +%} + +### 9.2 CORS – проверка на разрешение любого origin +OPTIONS {{baseUrl}}/register +Origin: https://evil.com + +> {% + client.test("CORS not enabled (default secure)", function() { + client.assert(!response.headers["Access-Control-Allow-Origin"], + "Default CORS is secure – no wildcard"); + }); +%} \ No newline at end of file