diff --git a/PRD.md b/PRD.md index 798c733..a12222c 100644 --- a/PRD.md +++ b/PRD.md @@ -609,3 +609,33 @@ optional Redis/Memcached. - lookup by bit: found, 0, several bits, unknown bit; - `ig_detector_at` out of range; - registry `detect` gives the same verdicts as the direct functions. + +### 1.4 Normalizer: URL decoding, `%u`, NUL +- **Context:** + - PHP has already URL-decoded GET/POST/COOKIE once before the input filter, so `decode_rounds` (default 2) are **extra** rounds that catch double and triple encoding. + - `REQUEST_URI` is raw, so the same rounds give it its first decode. + - A copy is normalized; the original value is not touched. +- **Decision** (`src/core/ig_normalize.{h,c}`): + - **One round:** `ig_url_decode(in, len, out, flags)`. + - `%XX` → byte. + - `%uXXXX` (IIS, a common WAF bypass) → UTF-8, 1–3 bytes; surrogates are not decoded. + - `+` → space only with `IG_DECODE_PLUS` (query/body; it is literal in a URI path). + - Invalid or truncated sequences are copied as is, like PHP does. + - The output is never longer than the input, so an `out` of `len` bytes is enough and in-place decoding is allowed. + - **Normalization:** `ig_normalize(in, len, rounds, flags, out, &stats)`. + - Each round decodes, then removes NUL bytes. NUL is removed in every round because `%%0041` becomes `%41` only after the NUL goes. + - It stops when a round changes nothing or the rounds run out. + - NUL removal also happens with `rounds = 0`. + - **Stats:** `rounds` (rounds that changed the value), `nul_removed`, and `limit_hit` (the value would still decode). The anomaly event for `limit_hit` comes with class 3 (v1.1). + - **No allocation** in the core: the caller provides the buffer (`emalloc` in the Zend layer, phase 4). Stateless and ZTS-safe. + - **Out of scope:** overlong UTF-8 and invalid encodings (class 3), HTML entities (1.5), length limits (1.6). +- **Tests first** (`tests/unit/test_normalize.c`): + - one round: + - hex in both cases; invalid and truncated sequences kept; + - `%u0041`, `%u0410`, `%u20AC`; a surrogate and an invalid `%u` kept; + - `+` with and without the flag; in-place decoding; empty input. + - normalization: + - double encoding; triple encoding with 2 rounds (→ `limit_hit`); early stop; + - NUL raw and `%00`; `%%0041`; + - `rounds = 0`; the output is never longer; NULL arguments. + - Fuzz target `fuzz_normalize`: output ≤ input and NUL-free, in-place = separate buffer, a round never grows the value. diff --git a/PRD.ru.md b/PRD.ru.md index 15c4f70..3361ee9 100644 --- a/PRD.ru.md +++ b/PRD.ru.md @@ -1128,3 +1128,48 @@ SEVERITY = "info" | "low" | "medium" | "high" | "critical" ; - поиск по биту: найден, 0, несколько битов, неизвестный бит; - `ig_detector_at` вне диапазона; - `detect` из реестра — та же функция, что `ig_detect_sqli`/`ig_detect_xss`: тот же вердикт на атаке. + +### 1.4 Нормализатор: URL-decode, `%u`, NUL +**Контекст:** +- GET, POST и COOKIE PHP уже один раз раскодировал (`urldecode`) до input filter. Поэтому `decode_rounds` (по умолчанию 2) — это **дополнительные** раунды: они ловят двойное и тройное кодирование (`%2527` → `%27` → `'`). +- URI (`REQUEST_URI`) приходит сырым, для него те же раунды дают первое раскодирование. +- Нормализуется копия: исходное значение не меняется (FR-1.3). + +**Решение (`src/core/ig_normalize.{h,c}`):** +- **Один раунд:** `size_t ig_url_decode(const char *in, size_t len, char *out, unsigned flags)`. + - `%XX` (hex в любом регистре) → байт. + - `%uXXXX` (IIS/старые IE, частый приём обхода WAF) → кодовая точка в UTF-8, 1–3 байта; суррогаты `D800–DFFF` не раскодируются. + - `+` → пробел только с флагом `IG_DECODE_PLUS`: как `urldecode()` для query и тела. Для пути URI `+` — литерал. + - Невалидные и обрезанные последовательности (`%zz`, `%4` в конце, `%u12`) копируются как есть, ровно как в PHP. + - Результат никогда не длиннее входа: `%XX` даёт 1 байт из 3, `%uXXXX` — не более 3 из 6. Буфер `out` размером `len` достаточен, `out == in` (на месте) допустим. +- **Нормализация:** `ig_normalize(in, len, rounds, flags, out, &stats)` → длина результата. + - Раунд = URL-decode, затем удаление NUL-байтов. + - NUL удаляется в каждом раунде, а не только в конце: иначе `%%0041` → `%`+NUL+`41` → после удаления `%41` — новая последовательность, которую должен увидеть следующий раунд. + - Остановка — когда раунд ничего не изменил или раунды кончились. + - NUL удаляется и при `rounds = 0`. +- **Статистика `ig_norm_stats_t`:** + - `rounds` — сколько раундов изменили значение; + - `nul_removed` — сколько удалено NUL; + - `limit_hit` — после последнего разрешённого раунда значение всё ещё раскодируется. + + Глубокое кодирование само по себе подозрительно; событие аномалии будет в классе 3 (v1.1), сейчас флаг только возвращается. +- **Без выделения памяти** в ядре: буфер даёт вызывающий (Zend-слой — `emalloc`, фаза 4). Состояния нет, функции безопасны для ZTS. +- **Вне задачи:** + - overlong UTF-8 (`%c0%af`) и невалидные кодировки — класс 3 (аномалии ввода); + - HTML-сущности — 1.5; + - ограничение длины — 1.6. + +**Тесты (red первыми), `tests/unit/test_normalize.c`:** +- Один раунд: + - `%XX` в верхнем и нижнем регистре; невалидные и обрезанные последовательности сохраняются; + - `%u0041` → `A`, `%u0410` → `А` (UTF-8), `%u20AC` → `€`; суррогат и невалидный `%u` сохраняются; + - `+` с флагом и без; раскодирование на месте; пустой вход. +- Нормализация: + - `%2527` при 2 раундах → `'`; тройное кодирование при 2 раундах → `%27` и `limit_hit`; + - ранняя остановка (`rounds` в статистике); + - удаление NUL: сырой байт и `%00`; случай `%%0041`; + - `rounds = 0`; результат не длиннее входа; NULL-аргументы. +- Fuzz-таргет `fuzz_normalize`: + - результат ≤ входа и без NUL; + - на месте = в отдельный буфер; + - один раунд никогда не увеличивает длину. diff --git a/TASKS.md b/TASKS.md index 89970a6..c503e7a 100644 --- a/TASKS.md +++ b/TASKS.md @@ -53,7 +53,7 @@ - [x] **1.1 [TDD]** `ig_detect_sqli()` wrapper over libinjection, fingerprint in result. - [x] **1.2 [TDD]** `ig_detect_xss()` wrapper. - [x] **1.3 [TDD]** Common detector interface and registry. -- [ ] **1.4 [TDD]** Normalizer: repeated URL decoding, NUL removal, `%u` sequences. +- [x] **1.4 [TDD]** Normalizer: repeated URL decoding, NUL removal, `%u` sequences. - [ ] **1.5 [TDD]** HTML entity decoding for XSS. - [ ] **1.6 [TDD]** Length limit with `truncated` flag. - [ ] **1.7 [TDD]** Glob matcher for exclusions. diff --git a/TASKS.ru.md b/TASKS.ru.md index 7ba7af8..489999d 100644 --- a/TASKS.ru.md +++ b/TASKS.ru.md @@ -54,7 +54,7 @@ - [x] **1.1 [TDD]** Обёртка `ig_detect_sqli(const char*, size_t, ig_result_t*)` над libinjection: тесты на классические payload'ы (`' OR 1=1--`, `UNION SELECT`, `; DROP TABLE`) и на безопасные строки. Fingerprint в результате. (FR-2.1) - [x] **1.2 [TDD]** Обёртка `ig_detect_xss(...)`: `