diff --git a/PRD.md b/PRD.md index a12222c..faf8aa3 100644 --- a/PRD.md +++ b/PRD.md @@ -639,3 +639,28 @@ optional Redis/Memcached. - 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. + +### 1.5 HTML entity decoding for XSS +- **Context:** + - Browsers decode character references in attribute values before parsing a URL, so `href="javascript:alert(1)"` and `java script:alert(1)` run, while `ig_detect_xss` misses them in the raw value (note to 1.2). + - Pipeline order (wired in phases 2/4): URL normalization (1.4) → HTML decoding → `ig_detect_xss` on the decoded copy. This also catches `%26%23106%3B` → `j` → `j`. +- **Decision** (`src/core/ig_normalize.{h,c}`): `size_t ig_html_decode(const char *in, size_t len, char *out)`, one pass, HTML5 character reference rules. + - **Numeric:** `&#` + decimal digits or `&#x`/`&#X` + hex digits; `;` optional; any number of leading zeros. + - The code point is written as UTF-8 (1–4 bytes). + - `0`, surrogates and values above `10FFFF` (including overflow) become U+FFFD, as in browsers, so no NUL appears. + - No digits (`&#;`, `&#x;`) → copied as is. The windows-1252 mapping of `€`–`Ÿ` is out of scope. + - **Named:** a subset of the 2231 HTML5 names that yields attack-relevant characters. + - Legacy names also accepted without `;`: `amp`, `lt`, `gt`, `quot` (and `AMP`, `LT`, `GT`, `QUOT`), `nbsp`. + - `;` required: `apos`, `colon`, `lpar`, `rpar`, `Tab`, `NewLine`, `sol`, `bsol`, `grave`, `equals`, `semi`, `comma`, `period`, `excl`, `quest`, `num`, `percnt`, `plus`, `ast`, `commat`, `lowbar`, `verbar`, `vert`, `lsqb`, `lbrack`, `rsqb`, `rbrack`, `lcub`, `lbrace`, `rcub`, `rbrace`, `dollar`, `Hat`. + - Case-sensitive as in HTML5; unknown names are copied as is. + - The attribute rule (a legacy name without `;` before an alphanumeric or `=` is not decoded) is not applied: the context is unknown and the detector should see more. The false-positive cost is measured by the corpus in 1.8. + - **The output is never longer than the input:** a numeric reference yields no more bytes than it occupies (U+FFFD is 3 bytes from at least `�`), and each named value is shorter than its name (checked by a test). An `out` of `len` bytes is enough; in-place decoding is allowed. + - **One pass.** Browsers expand `&#106;` once, and so do we; a second pass would turn safe text into markup. + - No allocation, no state, ZTS-safe. +- **Tests first** (`tests/unit/test_html_decode.c`): + - numeric: decimal and hex, both `x` cases, with and without `;`, leading zeros, multi-byte UTF-8; + - U+FFFD for `�`, a surrogate, `�` and overflow; no digits → kept; + - named: legacy with and without `;`, `;`-only names not decoded without `;`, case, unknown name, trailing `&`, `&<`; + - in place, empty input, NULL arguments; table: every value shorter than its name; + - with `ig_detect_xss`: `javascript:alert(1)`, `java script:alert(1)`, `<script>…` are ATTACK after decoding; `Tom & Jerry` is clean. + - Fuzz target `fuzz_html_decode`: output ≤ input, in place = separate buffer, no new NUL. diff --git a/PRD.ru.md b/PRD.ru.md index 3361ee9..f4a05d0 100644 --- a/PRD.ru.md +++ b/PRD.ru.md @@ -1173,3 +1173,32 @@ SEVERITY = "info" | "low" | "medium" | "high" | "critical" ; - результат ≤ входа и без NUL; - на месте = в отдельный буфер; - один раунд никогда не увеличивает длину. + +### 1.5 Декодирование HTML-сущностей для XSS +**Контекст:** +- Браузер раскодирует сущности в значениях атрибутов до разбора URL. Поэтому `href="javascript:alert(1)"` и `java script:alert(1)` исполняются, а `ig_detect_xss` по сырому значению их не видит (заметка к 1.2). +- Порядок в конвейере проверки (подключение — фазы 2/4): URL-нормализация (1.4) → HTML-декодирование → `ig_detect_xss` по раскодированной копии. Так ловится и `%26%23106%3B` → `j` → `j`. + +**Решение (`src/core/ig_normalize.{h,c}`):** +- `size_t ig_html_decode(const char *in, size_t len, char *out)` — один проход, по правилам HTML5 для ссылок на символы. +- **Числовые ссылки:** `&#` + десятичные цифры или `&#x`/`&#X` + hex-цифры, `;` необязательна, ведущие нули допустимы в любом количестве. + - Результат — кодовая точка в UTF-8 (1–4 байта). + - `0`, суррогаты `D800–DFFF` и значения > `10FFFF` (в том числе переполнение длинной записью) → U+FFFD, как в браузерах. NUL при этом не появляется. + - Без цифр (`&#;`, `&#x;`, `&#xg`) — копируется как есть. + - Таблица windows-1252 для `€`–`Ÿ` вне задачи: эти символы не участвуют в атаках. +- **Именованные сущности** — не вся таблица HTML5 (2231 имя), а подмножество, дающее символы, значимые для атак: + - legacy-имена, которые браузер принимает и без `;`: `amp`, `lt`, `gt`, `quot` (и `AMP`, `LT`, `GT`, `QUOT`), `nbsp`; + - только с `;`: `apos`, `colon`, `lpar`, `rpar`, `Tab`, `NewLine`, `sol`, `bsol`, `grave`, `equals`, `semi`, `comma`, `period`, `excl`, `quest`, `num`, `percnt`, `plus`, `ast`, `commat`, `lowbar`, `verbar`, `vert`, `lsqb`, `lbrack`, `rsqb`, `rbrack`, `lcub`, `lbrace`, `rcub`, `rbrace`, `dollar`, `Hat`. + - Имена чувствительны к регистру, как в HTML5. Неизвестное имя копируется как есть. + - Правило атрибутов (legacy-имя без `;` перед буквой, цифрой или `=` не раскодируется) не применяется: контекст неизвестен, детектор должен видеть больше. Цена в ложных срабатываниях измеряется корпусом в 1.8. +- **Результат никогда не длиннее входа:** числовая ссылка даёт не больше байт, чем занимает (U+FFFD — 3 байта из минимум 3 символов `�`), значение каждой именованной сущности короче её записи (проверяется тестом). Буфер `out` размером `len` достаточен, на месте допустимо. +- **Один проход.** Двойное кодирование (`&#106;`) браузер раскрывает один раз, и мы тоже. Повторное декодирование не делается: приложение, раскодирующее дважды, — редкость, а лишний проход превращает безопасный текст в разметку. +- Без выделения памяти и состояния, безопасно для ZTS. + +**Тесты (red первыми), `tests/unit/test_html_decode.c`:** +- числовые: десятичные и hex, оба регистра `x`, с `;` и без, ведущие нули, многобайтовый UTF-8 (`А`, `€`, `😀`); +- U+FFFD для `�`, суррогата, `�` и переполнения; без цифр — как есть; +- именованные: legacy с `;` и без, только-с-`;` без `;` не раскодируется, регистр, неизвестное имя, `&` в конце, `&<`; +- на месте, пустой вход, NULL-аргументы; таблица: значение короче записи; +- связка с `ig_detect_xss`: `javascript:alert(1)`, `java script:alert(1)`, `<script>alert(1)</script>` после декодирования — ATTACK; `Tom & Jerry` — чисто. +- Fuzz-таргет `fuzz_html_decode`: результат ≤ входа, на месте = в отдельный буфер, NUL не появляется. diff --git a/TASKS.md b/TASKS.md index c503e7a..4de3717 100644 --- a/TASKS.md +++ b/TASKS.md @@ -54,7 +54,7 @@ - [x] **1.2 [TDD]** `ig_detect_xss()` wrapper. - [x] **1.3 [TDD]** Common detector interface and registry. - [x] **1.4 [TDD]** Normalizer: repeated URL decoding, NUL removal, `%u` sequences. -- [ ] **1.5 [TDD]** HTML entity decoding for XSS. +- [x] **1.5 [TDD]** HTML entity decoding for XSS. - [ ] **1.6 [TDD]** Length limit with `truncated` flag. - [ ] **1.7 [TDD]** Glob matcher for exclusions. - [ ] **1.8 [TDD]** Corpus runner with accuracy thresholds (NFR-5). diff --git a/TASKS.ru.md b/TASKS.ru.md index 489999d..db92620 100644 --- a/TASKS.ru.md +++ b/TASKS.ru.md @@ -55,7 +55,7 @@ - [x] **1.2 [TDD]** Обёртка `ig_detect_xss(...)`: `