food-market/docs/sprint23-progress.md
nns 2bbd078659
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
docs(s23): итог — 4 bugs found, 4 fixed, all retested ✓
Severity breakdown:
- CRITICAL: 1 (race → 500 instead of 409)
- Medium: 1 (NUL byte → 500)
- Low: 2 (validator length mismatch, tiny price rounded to 0)

Retest на stage: 4/4 fix verified, stock invariant holds (20 parallel
sales → 6 ok + 14 conflict-409 + 0 500), smoke 5/5, catalog 6/6.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 01:42:28 +05:00

110 lines
6.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Sprint 23 — adversarial bug-hunt
Цель: атаковать систему как злонамеренный пользователь. После 22 спринтов
система зрелая, но скрытые баги ТОЧНО есть. Найти как можно больше,
сразу починить.
Старт: 2026-06-08. Исполнитель: Claude Opus 4.7.
## Категории атак
- [x] **1. Race conditions** — параллельные POST на posting-endpoints
- [x] **2. Auth edge** — JWT tampering / replay / SuperAdmin override
- [x] **3. Multi-tenant via URL** — cross-org доступ через прямой ID
- [x] **4. Validation bypass** — SQL/XSS/path-traversal/unicode
- [x] **5. Numeric/decimal edge** — отрицательные, overflow, precision
- [x] **6. Stock invariant** — 100 параллельных продаж
- [x] **7. DOS protection** — SignalR flood, large bodies, slowloris
- [x] **8. Hangfire-jobs safety** — admin-vs-superadmin, cross-tenant
## Итог: найдено 4 бага, починено 4
| # | Severity | Component | Симптом | Fix |
|---|---|---|---|---|
| **001** | Medium | `ProductInputValidator` + Counterparty | NUL-byte `\x00` в Name → 500 empty body (Postgres TEXT reject) | `NoControlChars()` extension в FluentValidation, применён ко всем строковым полям ProductInput + CounterpartyInput |
| **002** | Low | `ProductInputValidator.Name` | `MaximumLength(200)` vs `[StringLength(500)]` + `HasMaxLength(500)` — Name 201..499 chars отвергался ошибочно | Расширил validator до 500. Counterparty: 200 → 255 |
| **003** | **CRITICAL** | Все posting'и под Serializable (RetailSale.Post, Supply.Post, …) | 40001 serialization failure → middleware → 500 empty body | Global `SerializationConflictMiddleware` мапит 40001 → 409 `{error, retryable: true}`. `SerializableRetry` helper для явного retry (применён к RetailSale.Post → PostCoreAsync) |
| **004** | Low | `FindMissingRequiredPriceAsync` | Цена `0.0000001` округлялась RoundIfNeeded'ом в 0 уже ПОСЛЕ required-price-check | Перенёс RoundIfNeeded внутрь check'a |
## Чисто (no bugs)
| Категория | Тестовый сценарий | Результат |
|---|---|---|
| Multi-tenant cross-org | B GET/PUT/DELETE A's product/CP/preset/export | 404 ✓ |
| Multi-tenant lists | B видит свои exports/audit/stores/orgs | isolated ✓ |
| Multi-tenant bulk-update | B `{ids:[A's id], op:archive}` | `affected=0`, не утечка ✓ |
| Auth: garbage JWT | `Bearer abc.def.ghi` | 401 ✓ |
| Auth: tampered JWT | подложенный payload | 401 ✓ |
| Auth: Basic вместо Bearer | `Authorization: Basic xxx` | 401 ✓ |
| Auth: длинный header (100KB) | nginx закрывает | connection reset ✓ |
| Auth: refresh-token forged | `grant_type=refresh_token, refresh_token=zzz` | 400 invalid_grant ✓ |
| Auth: SuperAdmin override как Admin | `X-Org-Override: B` от Admin A | ignored, видит A ✓ |
| Auth: garbage UUID в Override | `X-Org-Override: not-a-uuid` | 200, нет 500 ✓ |
| Auth: CORS evil.com | OPTIONS /api/me с `Origin: https://evil.com` | нет `allow-origin` ✓ |
| Validation: SQL-inj в search | `?search='OR'1'='1` | 200 total=0 (parameterized) ✓ |
| Validation: XSS в name | `<script>...</script>` | stored as literal (фронт escape'ит на render) ✓ |
| Validation: JSON-injection | `\",\"isArchived\":true,\"x\":\"` | stored as literal string ✓ |
| Validation: 10MB body | nginx | 413 Payload Too Large ✓ |
| Validation: 10000 JSON keys | unknown-keys ignore | OK ✓ |
| Numeric: -100 | invalid | 400 ✓ |
| Numeric: 1e20 overflow | Range(0, 1e10) | 400 ✓ |
| Numeric: 0 цена required | `FindMissingRequiredPriceAsync` | 400 ✓ |
| **Stock invariant** | 20 parallel sales at stock=10 | **stock=10 == Σ movements, no oversell** ✓ |
| DOS: 200 custom headers | nginx | 431 Request Header Fields Too Large ✓ |
| Hangfire dashboard как Admin | `/hangfire` | 403 ✓ |
| Hangfire dashboard без auth | `/hangfire` | 401 ✓ |
| seed-demo с `organizationId` в body | tenant context overrides | seed только в свою org ✓ |
## Retest после фиксов
- **Bug #001**: NULL-byte в name → **400** with valid message ✓
- **Bug #002**: name 499 chars → **201** (был 400) ✓
- **Bug #004**: price 0.0000001 → **400** "обязательна > 0" ✓
- **Bug #003**: 20 parallel sales — **500 = 0** (было 12), 6 ok + 14 conflict 409, stock invariant сохраняется (4 == Σ movements) ✓
**stage scenarios после деплоя**: smoke 5/5 ✓, catalog 6/6 ✓.
## Severity breakdown
- **CRITICAL**: 1 (bug-003 — visible 500 на каждый race на проде)
- **Medium**: 1 (bug-001 — 500 без причины при использовании spec-chars)
- **Low**: 2 (bug-002 validator inconsistency, bug-004 business-logic edge)
## Журнал
### 2026-06-08 старт
Sprint 22 закрыт (7/7 ✓). Поехали по adversarial-attacks.
### 2026-06-08 hunt
Прошёл все 8 категорий через Python-скрипты (requests + concurrent.futures
для параллельной нагрузки). Найдено 4 бага в 2 категориях: validation
(bugs 001/002/004) + race (bug 003). Multi-tenant, auth, DOS, hangfire
— все clean.
### 2026-06-08 fix + retest
4 фикса в одном коммите:
- `SerializableRetry` helper (exp backoff + 40001 catch)
- `SerializationConflictMiddleware` (registered ПОСЛЕ SecurityHeaders, ДО Serilog)
- `ValidationExt.NoControlChars` + расширение MaxLength
- `FindMissingRequiredPriceAsync` с RoundIfNeeded
Retest на stage: все 4 ✓, smoke 5/5, catalog 6/6.
## Что НЕ найдено (но искал)
- Все cross-tenant attacks через прямой ID — query-filter держит.
- Все JWT-tampering — OpenIddict-validation rejects (encrypted JWE без
ключа нельзя forge).
- SQL-injection через ToLower().Contains() — EF Core параметризует.
- Stock invariant под нагрузкой — Serializable + Stock-cache держит,
oversell ноль из 20 попыток.
- /api/admin/seed-demo cross-tenant — tenant context из JWT, body ignored.
## Заключение
Все 4 найденных бага починены и retest'ены. Severity-breakdown:
**1 CRITICAL** (race → 500), **1 Medium** (NUL → 500), **2 Low** (validator,
business rule). Все исправления — без data-loss, без breaking changes для UI.
Watchdog `~/.fm-watchdog/DONE` создан.