@runsec/mcp 1.0.35 → 1.0.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/data/.rag-cache.json +1 -0
  2. package/dist/data/skills/_exploit_overrides.json +16 -0
  3. package/dist/data/skills/advanced-agent-cloud/index.md +94 -0
  4. package/dist/data/skills/advanced-agent-cloud/patterns.md +46 -0
  5. package/dist/data/skills/advanced-agent-cloud/skill.json +38 -0
  6. package/dist/data/skills/app-logic/index.md +69 -0
  7. package/dist/data/skills/app-logic/patterns.md +23 -0
  8. package/dist/data/skills/app-logic/skill.json +24 -0
  9. package/dist/data/skills/auth-keycloak/index.md +69 -0
  10. package/dist/data/skills/auth-keycloak/patterns.md +46 -0
  11. package/dist/data/skills/auth-keycloak/skill.json +51 -0
  12. package/dist/data/skills/browser-agent/index.md +58 -0
  13. package/dist/data/skills/browser-agent/patterns.md +15 -0
  14. package/dist/data/skills/browser-agent/skill.json +24 -0
  15. package/dist/data/skills/cloud-secrets/index.md +66 -0
  16. package/dist/data/skills/cloud-secrets/patterns.md +19 -0
  17. package/dist/data/skills/cloud-secrets/skill.json +28 -0
  18. package/dist/data/skills/csharp-dotnet/index.md +103 -0
  19. package/dist/data/skills/csharp-dotnet/patterns.md +270 -0
  20. package/dist/data/skills/csharp-dotnet/skill.json +27 -0
  21. package/dist/data/skills/desktop-vsto-suite/index.md +202 -0
  22. package/dist/data/skills/desktop-vsto-suite/patterns.md +154 -0
  23. package/dist/data/skills/desktop-vsto-suite/skill.json +26 -0
  24. package/dist/data/skills/devops-security/index.md +64 -0
  25. package/dist/data/skills/devops-security/patterns.md +23 -0
  26. package/dist/data/skills/devops-security/skill.json +42 -0
  27. package/dist/data/skills/domain-access-management/index.md +123 -0
  28. package/dist/data/skills/domain-access-management/patterns.md +58 -0
  29. package/dist/data/skills/domain-access-management/skill.json +36 -0
  30. package/dist/data/skills/domain-data-privacy/index.md +98 -0
  31. package/dist/data/skills/domain-data-privacy/patterns.md +48 -0
  32. package/dist/data/skills/domain-data-privacy/skill.json +36 -0
  33. package/dist/data/skills/domain-input-validation/index.md +210 -0
  34. package/dist/data/skills/domain-input-validation/patterns.md +158 -0
  35. package/dist/data/skills/domain-input-validation/skill.json +24 -0
  36. package/dist/data/skills/domain-platform-hardening/index.md +169 -0
  37. package/dist/data/skills/domain-platform-hardening/patterns.md +96 -0
  38. package/dist/data/skills/domain-platform-hardening/skill.json +27 -0
  39. package/dist/data/skills/ds-ml-security/patterns.md +137 -0
  40. package/dist/data/skills/fastapi-async/index.md +83 -0
  41. package/dist/data/skills/fastapi-async/patterns.md +329 -0
  42. package/dist/data/skills/fastapi-async/skill.json +32 -0
  43. package/dist/data/skills/frontend-react/index.md +26 -0
  44. package/dist/data/skills/frontend-react/patterns.md +226 -0
  45. package/dist/data/skills/frontend-react/skill.json +24 -0
  46. package/dist/data/skills/go-core/index.md +86 -0
  47. package/dist/data/skills/go-core/patterns.md +272 -0
  48. package/dist/data/skills/go-core/skill.json +22 -0
  49. package/dist/data/skills/hft-cpp-security/patterns.md +37 -0
  50. package/dist/data/skills/index.md +73 -0
  51. package/dist/data/skills/infra-k8s-helm/index.md +138 -0
  52. package/dist/data/skills/infra-k8s-helm/patterns.md +279 -0
  53. package/dist/data/skills/infra-k8s-helm/skill.json +41 -0
  54. package/dist/data/skills/integration-security/index.md +73 -0
  55. package/dist/data/skills/integration-security/patterns.md +132 -0
  56. package/dist/data/skills/integration-security/skill.json +30 -0
  57. package/dist/data/skills/java-enterprise/index.md +31 -0
  58. package/dist/data/skills/java-enterprise/patterns.md +816 -0
  59. package/dist/data/skills/java-enterprise/skill.json +26 -0
  60. package/dist/data/skills/java-spring/index.md +65 -0
  61. package/dist/data/skills/java-spring/patterns.md +22 -0
  62. package/dist/data/skills/java-spring/skill.json +23 -0
  63. package/dist/data/skills/license-compliance/index.md +58 -0
  64. package/dist/data/skills/license-compliance/patterns.md +12 -0
  65. package/dist/data/skills/license-compliance/skill.json +28 -0
  66. package/dist/data/skills/mobile-security/patterns.md +42 -0
  67. package/dist/data/skills/nodejs-nestjs/index.md +71 -0
  68. package/dist/data/skills/nodejs-nestjs/patterns.md +288 -0
  69. package/dist/data/skills/nodejs-nestjs/skill.json +24 -0
  70. package/dist/data/skills/observability/index.md +68 -0
  71. package/dist/data/skills/observability/patterns.md +22 -0
  72. package/dist/data/skills/observability/skill.json +26 -0
  73. package/dist/data/skills/php-security/patterns.md +202 -0
  74. package/dist/data/skills/ru-regulatory/index.md +72 -0
  75. package/dist/data/skills/ru-regulatory/patterns.md +28 -0
  76. package/dist/data/skills/ru-regulatory/skill.json +53 -0
  77. package/dist/data/skills/ruby-rails/index.md +65 -0
  78. package/dist/data/skills/ruby-rails/patterns.md +172 -0
  79. package/dist/data/skills/ruby-rails/skill.json +24 -0
  80. package/dist/data/skills/rust-security/patterns.md +152 -0
  81. package/dist/data/trufflehog-config.yaml +407 -0
  82. package/dist/index.js +3766 -372
  83. package/package.json +1 -1
@@ -0,0 +1,16 @@
1
+ {
2
+ "FAS-031": "Атакующий присылает файл/строку с ячейкой вроде =SUM(1+1) cmd|' /C calc'!A0; при открытии в Excel формула может выполнить команду на машине жертвы (CWE-1236).",
3
+ "FAS-004": "Атакующий подставляет в SQL значения через f-string/конкатенацию; при отсутствии параметризации выполняет произвольный SQL (CWE-89).",
4
+ "FAS-005": "Атакующий подставляет в SQL значения через конкатенацию строк; при отсутствии bind-параметров выполняет произвольный SQL (CWE-89).",
5
+ "CSH-010": "Атакующий подсовывает XML с внешней сущностью/DTD; при включённом XmlResolver читает файлы или достаёт секреты (CWE-611 XXE).",
6
+ "CSH-006": "Атакующий контролирует ORDER BY через query string; подставляет выражение, ведущее к утечке данных или обходу логики (CWE-89).",
7
+ "INS-090": "Атакующий внедряет формулу в CSV до экспорта в xlsx; при открытии в Excel возможна команда/внешняя ссылка (CWE-1236).",
8
+ "INS-091": "Атакующий подсовывает извлечённый текст в innerHTML; при отсутствии DOMPurify возможен XSS (CWE-1027).",
9
+ "PY-003": "Атакующий передаёт сериализованный объект; при небезопасной десериализации выполняет код на сервере (CWE-502).",
10
+ "PY-004": "Атакующий подставляет команду в shell=True; выполняет произвольные команды ОС (CWE-78).",
11
+ "PY-007": "Атакующий указывает внутренний URL/метаданные; SSRF к IMDS или внутренним сервисам (CWE-918).",
12
+ "AAC-014": "Атакующий подделывает JSON от внешнего AI API; без схемы поля трактуются как доверенные (CWE-20/918 цепочки).",
13
+ "INF-5.10": "Атакующий исчерпывает CPU/RAM контейнера множеством запросов; при отсутствии limits — отказ в обслуживании (обычно не прямой RCE).",
14
+ "CSH-037": "Атакующий подаёт вход, провоцирующий катастрофический backtracking regex; возможен ReDoS (обычно не прямой RCE).",
15
+ "DJA-003": "Атакующий провоцирует ошибку; при DEBUG=True получает детали стека и конфигурации (CWE-489)."
16
+ }
@@ -0,0 +1,94 @@
1
+ # Advanced Agent & Cloud
2
+
3
+ ## Stack overview
4
+
5
+ Covers automation agents that combine **headless browsers** (Playwright), **JavaScript/Node** surfaces (Next.js public env), **Python** workers and queues (RQ/redis), **object storage** (S3/MinIO-style), **reverse proxies** (Nginx), **egress controls**, and **real-time** browser APIs (WebRTC). Metrics are prefixed **`AAC`**.
6
+
7
+ **Product alignment:** critical for **Dion Agent** — browser-bound automation, queues, and cloud-adjacent controls must stay aligned with these metrics.
8
+
9
+ ## Top threats
10
+
11
+ - **SSRF and navigation abuse** via browser automation (`AAC-001`, `AAC-008`).
12
+ - **Secret and PII leakage** through client bundles, traces, or logs (`AAC-002`, `AAC-003`, `AAC-009`).
13
+ - **Unsafe deserialization and queue trust** (`AAC-004`) and **weak object-store policy** (`AAC-005`).
14
+ - **Broken identity validation** against OIDC/Keycloak expectations (`AAC-006`).
15
+ - **Missing rate limits / DoS** at the edge (`AAC-007`) and **ambient capture** (`AAC-010`).
16
+
17
+ ## Pattern catalog
18
+
19
+ Complete Anti-Pattern / Safe-Pattern definitions live in [`patterns.md`](patterns.md). The table below is a **table of contents** by metric ID.
20
+
21
+ | ID | Metric | Stack |
22
+ |---|---|---|
23
+ | `AAC-001` | SSRF in Playwright | `ALLOWED_HOSTS = {"cdn.example.com"}` `u = urlparse(userInput)` `if u.hostname not in ALLOWED_HOSTS: raise ValueError("url not allowed")` `await page.goto(userInput)` |
24
+ | `AAC-002` | Leakage in Playwright Traces | `await context.tracing.start(screenshots=False)` или маскирование PII перед экспортом трейса |
25
+ | `AAC-003` | Next.js Client-side Secret Leak | `const key = process.env.STRIPE_SECRET_KEY` (только серверные модули / Route Handlers без `NEXT_PUBLIC_`) |
26
+ | `AAC-004` | Insecure RQ (Redis Queue) Job | `json.loads(raw_job)` или `msgpack.loads` + явная схема данных |
27
+ | `AAC-005` | Insecure MinIO Pre-signed URL | `expires=timedelta(seconds=45)` + проверка владельца объекта и `method` GET-only где возможно |
28
+ | `AAC-006` | Keycloak SSO Bypass | `claims = jwt.decode(token, key, audience=..., issuer=..., options={"verify_exp": True})` |
29
+ | `AAC-007` | Nginx Rate Limit Missing | `limit_req zone=api burst=20 nodelay;` в том же `location` или выше по цепочке |
30
+ | `AAC-008` | Egress Proxy Bypass (Squid) | `HTTP_PROXY`/`HTTPS_PROXY` заданы на уровне контейнера; `Session(trust_env=True)` |
31
+ | `AAC-009` | Log Injection in Task Queues | `logger.info("job=%s", sanitize(redis_raw_payload))` |
32
+ | `AAC-010` | Insecure WebRTC/VAD Permissions | state-machine: явное `consentGiven === true` до вызова `getUserMedia` |
33
+ | `AAC-011` | MCP tool path/command handling without `abspath` and workspace boundary checks | Use strict Pydantic BaseModel schemas for input/output, including response_model and field allowlists. |
34
+ | `AAC-012` | MCP tool arguments without Pydantic validation schema | Use strict Pydantic BaseModel schemas for input/output, including response_model and field allowlists. |
35
+ | `AAC-013` | Non-atomic Redis read-modify-write in RQ workers without `WATCH`/Lua | Use strict Pydantic BaseModel schemas for input/output, including response_model and field allowlists. |
36
+ | `AAC-014` | Missing `pydantic.BaseModel` validation for JSON responses from external AI APIs | Use strict Pydantic BaseModel schemas for input/output, including response_model and field allowlists. |
37
+ | `AAC-015` | Logging full system prompts/context windows into observability sinks (CWE-1037) | Логировать только redacted metadata (hash/id/length), исключать system prompts и полный контекст из telemetry/logs by default. |
38
+ | `AAC-016` | Persisting raw model memory snapshots with secrets in traces (CWE-1037) | Перед трассировкой удалять секреты/PII из agent memory и применять denylist sensitive keys (`token`, `secret`, `api_key`). |
39
+ | `AAC-017` | Exporting chain-of-thought/internal reasoning to logs (CWE-1037) | Не логировать скрытые reasoning fields; сохранять только конечный ответ и технические метрики выполнения. |
40
+ | `AAC-018` | Storing unencrypted prompt history in persistent debug artifacts (CWE-1037) | Шифровать артефакты или отключать persistent prompt dumps в production; включать retention + secure erase policy. |
41
+ | `AAC-019` | MCP tool registry allows wildcard command execution (CWE-693) | Use strict Pydantic BaseModel schemas for input/output, including response_model and field allowlists. |
42
+ | `AAC-020` | Missing tenant isolation in shared vector store namespaces | Использовать server-side tenant mapping и immutable namespace binding, запретить namespace from external input. |
43
+ | `AAC-021` | Prompt template loaded from user-controlled path | Use strict Pydantic BaseModel schemas for input/output, including response_model and field allowlists. |
44
+ | `AAC-022` | Unbounded retry loops on AI API errors | Use strict Pydantic BaseModel schemas for input/output, including response_model and field allowlists. |
45
+ | `AAC-023` | External tool result injected into system role without sanitization | Нормализовать/санитизировать tool output и разделять роли (`system` immutable, external data only as user/tool role). |
46
+ | `AAC-024` | Agent callbacks execute network requests without egress allowlist | Вводить egress allowlist + DNS/IP validation и блокировать link-local/metadata ranges перед запросом. |
47
+ | `AAC-025` | Cache key for AI results misses auth/tenant dimension | Включать tenant/user scope и policy version в cache key, чтобы исключить cross-tenant data leakage. |
48
+ | `AAC-026` | Deserializing tool payload via `eval`/unsafe parser in agent runtime | Использовать безопасный JSON parser + strict schema validation и reject unknown fields до обработки. |
49
+ | `AAC-027` | LLM indirect injection: Base64 decode обработки ответа модели до валидации схемы (CWE-1... | Pydantic `model_validate` на сыром тексте ответа; для base64 — только после явного поля в схеме и max length; в TS — Zod + отказ от `Buffer.from(llm, "base64")` без policy. |
50
+ | `AAC-028` | LLM indirect injection: Hex / binary decode текста ассистента (CWE-1109) | Как AAC-027: строгая схема ответа, без `fromhex`/`hex` decode на необработанной строке модели. |
51
+ | `AAC-029` | Обфусцированная shell-команда в ответе LLM: `chr`/`ord` цепочка (CWE-1027) | Pydantic-схема ответа без `exec`/`system`; sandbox tool API. |
52
+ | `AAC-030` | Обфусцированная команда: `base64` + `eval` в JS от ассистента (CWE-1334) | Zod + deny `eval`/`Function` на данных модели. |
53
+ | `AAC-031` | PowerShell `-EncodedCommand` из текста модели (CWE-78) | Allowlist argv; no shell metacharacters from model. |
54
+ | `AAC-032` | Bash обёртка `$(echo ... \ | Парсер JSON + фиксированные команды. |
55
+ | `AAC-033` | `compile()` / `exec()` на обфусцированном Python из модели (CWE-94) | RestrictedPython или no code execution path. |
56
+ | `AAC-034` | Unicode homoglyph obfuscation в «команде» ассистента (CWE-1335) | Unicode NFKC + command allowlist. |
57
+ | `AAC-035` | Разделённая на части команда (`list` concat) из LLM (CWE-1027) | Фиксированный `argv` template; модель только заполняет allowlisted args. |
58
+ | `AAC-036` | Hex-строка как «данные», исполняемые через `bytes.decode` + `exec` (CWE-1337) | Schema deny raw hex blobs. |
59
+ | `AAC-037` | Obfuscated URL scheme в ответе (`data:`/`javascript:`) для tool callback (CWE-1338) | Strict URL parser + block dangerous schemes. |
60
+ | `AAC-038` | Многослойное кодирование: `b64decode` → `zlib` → `exec` (CWE-1340) | Single JSON schema; no chained decoders on model text. |
61
+ | `AAC-039` | Python: `codecs.decode` hex из поля ответа LLM до схемы (CWE-1027) | Pydantic `model_validate` на тексте; deny `codecs.decode` на LLM fields. |
62
+ | `AAC-040` | JS: `Buffer.from` base64 из текста ассистента → `eval` (CWE-1027) | Zod schema; block `eval`/`new Function` on model-derived buffers. |
63
+ | `AAC-041` | Python: `binascii.a2b_hex` на строке модели → `pickle.loads` (CWE-1109) | JSON-only; no binary deserialization from assistant output. |
64
+ | `AAC-042` | JS: `Uint8Array.from(atob(llmB64), ...)` без валидации схемы (CWE-1109) | Reject raw atob blobs unless schema allows with max length. |
65
+ | `AAC-043` | JWT из интеграции: `exp` > 24h на machine tokens (CWE-613) | Короткий TTL + rotation; явные `leeway`/`max_age` в verifier. |
66
+ | `AAC-044` | SSRF guard: прямой `httpx` к Azure IMDS из agent-кода (CWE-918) | Central HTTP wrapper; no IMDS literals in agent runtime. |
67
+
68
+ ## Verification
69
+
70
+ **Verification:** Check the gold testbed file(s) below for `Vulnerable: <ID>` markers (static Semgrep + `detection-matrix.md` ground truth).
71
+
72
+ - [`gold-standard-testbed/aac_vulnerable.py`](../gold-standard-testbed/aac_vulnerable.py)
73
+ - [`gold-standard-testbed/aac_vulnerable.ts`](../gold-standard-testbed/aac_vulnerable.ts)
74
+
75
+ After changing [`patterns.md`](patterns.md), run from the repo root:
76
+
77
+ ```bash
78
+ python scripts/sync_semgrep.py
79
+ ```
80
+
81
+ ## Workflow: Recon → Scan → Verify
82
+
83
+ ### 1) Recon
84
+ - Map entrypoints, data flows, and trust boundaries for this stack.
85
+ - Identify which metrics in [`patterns.md`](patterns.md) apply to the code under review.
86
+
87
+ ### 2) Scan
88
+ - Run Semgrep with `semgrep-rules/<skill>.yaml` (generated) and correlate with Anti-Patterns.
89
+ - Eliminate findings that cannot bind to a metric row.
90
+
91
+ ### 3) Verify
92
+ - Confirm markers or scanner hits for touched IDs in the gold testbed when adding metrics.
93
+ - Emit findings as `Vulnerable: <PREFIX>-<NNN>` in written reviews.
94
+
@@ -0,0 +1,46 @@
1
+ | ID | Название метрики | Anti-Pattern (Vulnerable Code/YAML) | Safe-Pattern (Remediation) | Stack | Источник fix_template | Exploit scenario |
2
+ |---|---|---|---|---|---|---|
3
+ | AAC-001 | SSRF in Playwright | `await page.goto(userInput)` | `ALLOWED_HOSTS = {"cdn.example.com"}`<br>`u = urlparse(userInput)`<br>`if u.hostname not in ALLOWED_HOSTS: raise ValueError("url not allowed")`<br>`await page.goto(userInput)` | Agent/Browser | `CWE-918` | `ALLOWED_HOSTS = {"cdn.example.com"}` `u = urlparse(userInput)` `if u.hostname not in ALLOWED_HOSTS: raise ValueError("url not allowed")` `await page.goto(userInput)` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: aac-001 ssrf playwright await page goto userinput allowed hosts cdn example com u urlparse if hostname not raise valueerror url -->
4
+ | AAC-002 | Leakage in Playwright Traces | `await context.tracing.start(screenshots=True)` | `await context.tracing.start(screenshots=False)` или маскирование PII перед экспортом трейса | Agent/Browser | `CWE-200` | `await context.tracing.start(screenshots=False)` или маскирование PII перед экспортом трейса | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: aac-002 leakage playwright traces await context tracing start screenshots true false или маскирование pii перед экспортом трейса -->
5
+ | AAC-003 | Next.js Client-side Secret Leak | `const key = process.env.NEXT_PUBLIC_STRIPE_SECRET` | `const key = process.env.STRIPE_SECRET_KEY` (только серверные модули / Route Handlers без `NEXT_PUBLIC_`) | Agent/Browser | `CWE-20` | `const key = process.env.STRIPE_SECRET_KEY` (только серверные модули / Route Handlers без `NEXT_PUBLIC_`) | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: aac-003 next js client side secret leak const key process env public stripe только серверные модули route handlers без -->
6
+ | AAC-004 | Insecure RQ (Redis Queue) Job | `pickle.loads(raw_job)` | `json.loads(raw_job)` или `msgpack.loads` + явная схема данных | Agent/Browser | `CWE-502` | `json.loads(raw_job)` или `msgpack.loads` + явная схема данных | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: aac-004 insecure rq redis queue job pickle loads raw json или msgpack явная схема данных -->
7
+ | AAC-005 | Insecure MinIO Pre-signed URL | `client.presigned_put_object("b", "o", expires=timedelta(days=7))` | `expires=timedelta(seconds=45)` + проверка владельца объекта и `method` GET-only где возможно | Agent/Browser | `CWE-639` | `expires=timedelta(seconds=45)` + проверка владельца объекта и `method` GET-only где возможно | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: aac-005 insecure minio pre signed url client presigned put object b o expires timedelta days 7 seconds 45 проверка владельца объекта -->
8
+ | AAC-006 | Keycloak SSO Bypass | `if authorization:`<br>` headers = {"Authorization": authorization}` | `claims = jwt.decode(token, key, audience=..., issuer=..., options={"verify_exp": True})` | Agent/Browser | `CWE-287` | `claims = jwt.decode(token, key, audience=..., issuer=..., options={"verify_exp": True})` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: aac-006 keycloak sso bypass if authorization headers claims jwt decode token key audience issuer options verify exp true -->
9
+ | AAC-007 | Nginx Rate Limit Missing | `location /api/ {`<br>` proxy_pass http://backend;`<br>`}` | `limit_req zone=api burst=20 nodelay;` в том же `location` или выше по цепочке | Agent/Browser | `CWE-770` | `limit_req zone=api burst=20 nodelay;` в том же `location` или выше по цепочке | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: aac-007 nginx rate limit missing location api proxy pass http backend req zone burst 20 nodelay в том же или выше -->
10
+ | AAC-008 | Egress Proxy Bypass (Squid) | `requests.get(url, proxies={"http": None, "https": None})` | `HTTP_PROXY`/`HTTPS_PROXY` заданы на уровне контейнера; `Session(trust_env=True)` | Agent/Browser | `CWE-918` | `HTTP_PROXY`/`HTTPS_PROXY` заданы на уровне контейнера; `Session(trust_env=True)` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: aac-008 egress proxy bypass squid requests get url proxies http none https заданы на уровне контейнера session trust env true -->
11
+ | AAC-009 | Log Injection in Task Queues | `logger.info("job=%s", redis_raw_payload)` | `logger.info("job=%s", sanitize(redis_raw_payload))` | Agent/Browser | `CWE-117` | `logger.info("job=%s", sanitize(redis_raw_payload))` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: aac-009 log injection task queues logger info job s redis raw payload sanitize -->
12
+ | AAC-010 | Insecure WebRTC/VAD Permissions | `navigator.mediaDevices.getUserMedia({ audio: true })` | state-machine: явное `consentGiven === true` до вызова `getUserMedia` | Agent/Browser | `CWE-20` | state-machine: явное `consentGiven === true` до вызова `getUserMedia` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: aac-010 insecure webrtc vad permissions navigator mediadevices getusermedia audio true state machine явное consentgiven до вызова -->
13
+ | AAC-011 | MCP tool path/command handling without `abspath` and workspace boundary checks | Инструмент принимает `file_path`/`command` и выполняет без `os.path.abspath()` + проверки префикса рабочей директории | Нормализовать путь через `os.path.abspath`, проверять принадлежность разрешенному workspace-root, блокировать выход за границы и ограничивать команды allowlist-ом. | Agent/MCP/Python | CWE Final Certification | Use strict Pydantic BaseModel schemas for input/output, including response_model and field allowlists. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. |
14
+ | AAC-012 | MCP tool arguments without Pydantic validation schema | `def tool_handler(args):` без модели `BaseModel`/валидации типов | Для каждого MCP-инструмента задавать Pydantic-модель аргументов, strict type validation и reject unknown fields до бизнес-логики. | Agent/MCP/Python | CWE Final Certification | Use strict Pydantic BaseModel schemas for input/output, including response_model and field allowlists. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. |
15
+ | AAC-013 | Non-atomic Redis read-modify-write in RQ workers without `WATCH`/Lua | `value = redis.get(key)`<br>`redis.set(key, int(value)+1)` в конкурентном воркере | Для конкурентных обновлений использовать `WATCH/MULTI/EXEC` или Lua-скрипты (`EVAL`) для атомарности и защиты от race condition. | Agent/MCP/Python | CWE Final Certification | Use strict Pydantic BaseModel schemas for input/output, including response_model and field allowlists. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. |
16
+ | AAC-014 | Missing `pydantic.BaseModel` validation for JSON responses from external AI APIs | `data = response.json()` и прямое использование `data["..."]` без схемы | Валидировать JSON-ответы внешних AI API через `BaseModel` (`model_validate`) и reject unknown/invalid fields до использования в логике. | Agent/LLM/Python | CWE Final Certification | Use strict Pydantic BaseModel schemas for input/output, including response_model and field allowlists. | Атакующий подделывает JSON от внешнего AI API; без схемы поля трактуются как доверенные (CWE-20/918 цепочки). | <!-- semantic_anchor: aac-014 pydantic json external ai api -->
17
+ | AAC-015 | Logging full system prompts/context windows into observability sinks (CWE-1037) | `logger.info("system_prompt=%s", system_prompt)`<br>`trace.set_attribute("llm.context", full_context)` | Логировать только redacted metadata (hash/id/length), исключать system prompts и полный контекст из telemetry/logs by default. | Agent/LLM/Observability | CWE Final Certification | Логировать только redacted metadata (hash/id/length), исключать system prompts и полный контекст из telemetry/logs by default. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. |
18
+ | AAC-016 | Persisting raw model memory snapshots with secrets in traces (CWE-1037) | `span.set_attribute("agent.memory", json.dumps(memory_state))` | Перед трассировкой удалять секреты/PII из agent memory и применять denylist sensitive keys (`token`, `secret`, `api_key`). | Agent/LLM/Observability | CWE Final Certification | Перед трассировкой удалять секреты/PII из agent memory и применять denylist sensitive keys (`token`, `secret`, `api_key`). | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. |
19
+ | AAC-017 | Exporting chain-of-thought/internal reasoning to logs (CWE-1037) | `logger.debug("reasoning=%s", llm_response["thinking"])` | Не логировать скрытые reasoning fields; сохранять только конечный ответ и технические метрики выполнения. | Agent/LLM/Observability | CWE Final Certification | Не логировать скрытые reasoning fields; сохранять только конечный ответ и технические метрики выполнения. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. |
20
+ | AAC-018 | Storing unencrypted prompt history in persistent debug artifacts (CWE-1037) | `open("debug/prompts.json","w").write(json.dumps(history))` | Шифровать артефакты или отключать persistent prompt dumps в production; включать retention + secure erase policy. | Agent/LLM/Observability | CWE Final Certification | Шифровать артефакты или отключать persistent prompt dumps в production; включать retention + secure erase policy. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. |
21
+ | AAC-019 | MCP tool registry allows wildcard command execution (CWE-693) | `@tool("run_any")`<br>`subprocess.run(user_cmd, shell=True)` | Для MCP tools использовать command allowlist, fixed argv templates и `shell=False` с strict argument validation. | Agent/MCP/Python | CWE Final Certification | Use strict Pydantic BaseModel schemas for input/output, including response_model and field allowlists. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. |
22
+ | AAC-020 | Missing tenant isolation in shared vector store namespaces | `index.upsert(vectors, namespace=request.user_input)` | Использовать server-side tenant mapping и immutable namespace binding, запретить namespace from external input. | Agent/RAG | CWE Final Certification | Использовать server-side tenant mapping и immutable namespace binding, запретить namespace from external input. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. |
23
+ | AAC-021 | Prompt template loaded from user-controlled path | `tpl = Path(req["template"]).read_text()` | Разрешать только template IDs из allowlist и читать файлы только из trusted template directory after canonicalization. | Agent/LLM/Python | CWE Final Certification | Use strict Pydantic BaseModel schemas for input/output, including response_model and field allowlists. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. |
24
+ | AAC-022 | Unbounded retry loops on AI API errors | `while True: resp = client.responses.create(...)` | Ограничить retries и backoff policy (`max_retries`, circuit breaker), фиксировать timeout budget per request. | Agent/LLM/Python | CWE Final Certification | Use strict Pydantic BaseModel schemas for input/output, including response_model and field allowlists. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. |
25
+ | AAC-023 | External tool result injected into system role without sanitization | `messages=[{"role":"system","content": tool_output}]` | Нормализовать/санитизировать tool output и разделять роли (`system` immutable, external data only as user/tool role). | Agent/LLM | CWE Final Certification | Нормализовать/санитизировать tool output и разделять роли (`system` immutable, external data only as user/tool role). | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. |
26
+ | AAC-024 | Agent callbacks execute network requests without egress allowlist | `requests.get(callback_url)` где `callback_url` из tool output | Вводить egress allowlist + DNS/IP validation и блокировать link-local/metadata ranges перед запросом. | Agent/Network | CWE Final Certification | Вводить egress allowlist + DNS/IP validation и блокировать link-local/metadata ranges перед запросом. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. |
27
+ | AAC-025 | Cache key for AI results misses auth/tenant dimension | `cache_key = f"{prompt_hash}"` | Включать tenant/user scope и policy version в cache key, чтобы исключить cross-tenant data leakage. | Agent/Cache | CWE Final Certification | Включать tenant/user scope и policy version в cache key, чтобы исключить cross-tenant data leakage. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. |
28
+ | AAC-026 | Deserializing tool payload via `eval`/unsafe parser in agent runtime | `payload = eval(tool_result)` | Использовать безопасный JSON parser + strict schema validation и reject unknown fields до обработки. | Agent/Runtime | CWE Final Certification | Использовать безопасный JSON parser + strict schema validation и reject unknown fields до обработки. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. |
29
+ | AAC-027 | LLM indirect injection: Base64 decode обработки ответа модели до валидации схемы (CWE-1027) | `raw = base64.b64decode(llm_json["blob"])` затем `json.loads(raw)`<br>`const buf = Buffer.from(assistantText, "base64")` | Не трактовать base64 от LLM как доверенный канал: сначала `model_validate` JSON-строки, запретить скрытые поля `blob`/`payload` без allowlist; не `exec`/`eval` после decode. | Agent/LLM/Python | `CWE-1027` | Pydantic `model_validate` на сыром тексте ответа; для base64 — только после явного поля в схеме и max length; в TS — Zod + отказ от `Buffer.from(llm, "base64")` без policy. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: aac-027 llm indirect injection base64 decode b64decode llm json blob buffer from assistanttext base64 model validate -->
30
+ | AAC-028 | LLM indirect injection: Hex / binary decode текста ассистента (CWE-1109) | `bytes.fromhex(llm_reply.strip())`<br>`Buffer.from(modelOut, "hex")` | Запретить произвольный hex/binary decode из текста LLM; только структурированный JSON + schema; любые binary поля — deny-by-default. | Agent/LLM | `CWE-1109` | Как AAC-027: строгая схема ответа, без `fromhex`/`hex` decode на необработанной строке модели. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: aac-028 llm indirect injection hex decode bytes fromhex buffer from modelout hex -->
31
+ | AAC-029 | Обфусцированная shell-команда в ответе LLM: `chr`/`ord` цепочка (CWE-1027) | `cmd = "".join(chr(c) for c in [114,109,32,45,114,102,32,47])`<br>`os.system(cmd)` | Запретить исполнение строк, собранных из ответа модели; allowlist действий. | Agent/LLM/Python | `CWE-1027` | Pydantic-схема ответа без `exec`/`system`; sandbox tool API. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: aac-029 obfuscated chr ord chain os system llm -->
32
+ | AAC-030 | Обфусцированная команда: `base64` + `eval` в JS от ассистента (CWE-1334) | `eval(atob(llmChunk))` | Не eval декодированного текста LLM; JSON-only tool results. | Agent/LLM/JS | `CWE-1334` | Zod + deny `eval`/`Function` на данных модели. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: aac-030 eval atob llmchunk obfuscated js -->
33
+ | AAC-031 | PowerShell `-EncodedCommand` из текста модели (CWE-78) | `subprocess.run(["powershell", "-EncodedCommand", llm_b64])` | Блокировать передачу LLM-текста в shell; только structured actions. | Agent/LLM | `CWE-78` | Allowlist argv; no shell metacharacters from model. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: aac-031 powershell encodedcommand llm b64 subprocess -->
34
+ | AAC-032 | Bash обёртка `$(echo ... \| base64 -d)` из ответа ИИ (CWE-78) | `os.system("bash -c \"$(echo " + llm_pipe + " \| base64 -d)\"")` | Запретить pipeline из LLM к shell. | Agent/LLM | `CWE-78` | Парсер JSON + фиксированные команды. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: aac-032 bash echo base64 decode llm pipe -->
35
+ | AAC-033 | `compile()` / `exec()` на обфусцированном Python из модели (CWE-94) | `exec(compile(llm_code, "<llm>", "exec"))` | Не компилировать код из LLM; использовать DSL с allowlist. | Agent/LLM/Python | `CWE-94` | RestrictedPython или no code execution path. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: aac-033 compile exec llm code obfuscated python -->
36
+ | AAC-034 | Unicode homoglyph obfuscation в «команде» ассистента (CWE-1335) | `cmd = llm_text.replace("а", "a")` затем `run(cmd)` | Нормализация + blocklist опасных токенов до `subprocess`. | Agent/LLM | `CWE-1335` | Unicode NFKC + command allowlist. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: aac-034 unicode homoglyph replace run cmd llm -->
37
+ | AAC-035 | Разделённая на части команда (`list` concat) из LLM (CWE-1027) | `parts = json.loads(llm_json["p"]); subprocess.run(parts)` | Запретить массив argv из модели без allowlist per position. | Agent/LLM/Python | `CWE-1027` | Фиксированный `argv` template; модель только заполняет allowlisted args. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: aac-035 json loads parts subprocess run llm -->
38
+ | AAC-036 | Hex-строка как «данные», исполняемые через `bytes.decode` + `exec` (CWE-1337) | `exec(bytes.fromhex(llm_hex).decode())` | Не исполнять декодированный hex из LLM. | Agent/LLM/Python | `CWE-1337` | Schema deny raw hex blobs. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: aac-036 exec bytes fromhex llm hex decode guardian -->
39
+ | AAC-037 | Obfuscated URL scheme в ответе (`data:`/`javascript:`) для tool callback (CWE-1338) | `open_url(llm_suggested_link)` где link содержит `javascript:` | Валидация URL scheme allowlist (`https` only). | Agent/LLM | `CWE-1338` | Strict URL parser + block dangerous schemes. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: aac-037 javascript scheme open url llm guardian -->
40
+ | AAC-038 | Многослойное кодирование: `b64decode` → `zlib` → `exec` (CWE-1340) | `exec(zlib.decompress(base64.b64decode(llm_blob)))` | Запретить цепочки декодирования из недоверенного LLM output. | Agent/LLM/Python | `CWE-1340` | Single JSON schema; no chained decoders on model text. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: aac-038 zlib decompress b64decode exec llm blob guardian -->
41
+ | AAC-039 | Python: `codecs.decode` hex из поля ответа LLM до схемы (CWE-1027) | `raw = codecs.decode(llm_obj["h"], "hex"); exec(raw.decode())` | Запретить произвольный hex codec на данных модели; только JSON + allowlist полей. | Agent/LLM/Python | `CWE-1027` | Pydantic `model_validate` на тексте; deny `codecs.decode` на LLM fields. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: aac-039 codecs decode llm h hex exec guardian -->
42
+ | AAC-040 | JS: `Buffer.from` base64 из текста ассистента → `eval` (CWE-1027) | `eval(Buffer.from(assistantChunk, "base64").toString("utf8"))` | Не исполнять декодированный контент LLM; статический tool API. | Agent/LLM/JS | `CWE-1027` | Zod schema; block `eval`/`new Function` on model-derived buffers. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: aac-040 buffer from assistantchunk base64 eval guardian -->
43
+ | AAC-041 | Python: `binascii.a2b_hex` на строке модели → `pickle.loads` (CWE-1109) | `pickle.loads(binascii.a2b_hex(llm_field))` | Запретить бинарные декоды из LLM в pickle/yaml unsafe loaders. | Agent/LLM/Python | `CWE-1109` | JSON-only; no binary deserialization from assistant output. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: aac-041 binascii a2b_hex llm pickle loads guardian -->
44
+ | AAC-042 | JS: `Uint8Array.from(atob(llmB64), ...)` без валидации схемы (CWE-1109) | `new Uint8Array(Uint8Array.from(atob(modelB64), c => c.charCodeAt(0)))` | Не строить byte arrays из необработанного base64 LLM; strict schema first. | Agent/LLM/JS | `CWE-1109` | Reject raw atob blobs unless schema allows with max length. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: aac-042 uint8array atob modelb64 guardian -->
45
+ | AAC-043 | JWT из интеграции: `exp` > 24h на machine tokens (CWE-613) | `jwt.encode({"sub": svc}, key, algorithm="HS256", headers={"exp": int(time.time()) + 86400 * 7})` | `exp` ≤ 24h для access; refresh отдельно; `authlib` TTL policy. | Agent/Integration | `CWE-613` | Короткий TTL + rotation; явные `leeway`/`max_age` в verifier. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: aac-043 jwt encode exp 7d machine token guardian -->
46
+ | AAC-044 | SSRF guard: прямой `httpx` к Azure IMDS из agent-кода (CWE-918) | `httpx.get("http://169.254.169.254/metadata/instance", headers={"Metadata": "true"})` | Только SDK/metadata-less path; denylist в egress для link-local. | Agent/Integration | `CWE-918` | Central HTTP wrapper; no IMDS literals in agent runtime. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: aac-044 httpx metadata instance header metadata true guardian -->
@@ -0,0 +1,38 @@
1
+ {
2
+ "skill_id": "advanced-agent-cloud",
3
+ "name": "Advanced Agent & Cloud",
4
+ "activation_triggers": [
5
+ "aac-playwright-egress",
6
+ "aac-next-public-env",
7
+ "aac-rq-pickle",
8
+ "aac-minio-presign",
9
+ "aac-nginx-rate",
10
+ "aac-webrtc-vad"
11
+ ],
12
+ "relevant_extensions": [
13
+ ".py",
14
+ ".ts",
15
+ ".js",
16
+ ".yaml",
17
+ ".yml"
18
+ ],
19
+ "tools": [
20
+ "semgrep",
21
+ "syft",
22
+ "trufflehog"
23
+ ],
24
+ "rules_path": "core/skills/advanced-agent-cloud/patterns.md",
25
+ "few_shot_examples": "core/gold-standard-testbed/aac_vulnerable.py",
26
+ "mitigation_logic": {
27
+ "NGX-storage": {
28
+ "exception_rule": "Если backend-код использует boto3.generate_presigned_url для выдачи временных ссылок доступа к объектам, отсутствие auth_request в Nginx считается архитектурно допустимым.",
29
+ "status_override": "OK",
30
+ "validation_requirements": [
31
+ "TTL presigned URL ограничен и не используется long-lived доступ.",
32
+ "Проверяется привязка к целевому object key и допустимому HTTP method."
33
+ ],
34
+ "rationale": "Контроль доступа переносится на криптографически подписанные временные URL, что устраняет обязательность промежуточной аутентификации на уровне Nginx."
35
+ }
36
+ },
37
+ "security_priority": 5
38
+ }
@@ -0,0 +1,69 @@
1
+ # Application Business Logic
2
+
3
+ ## Stack overview
4
+
5
+ Cross-cutting **BOLA/BOPLA**, workflow, webhook, and abuse-resistant business rules on typical FastAPI-style services. Metrics are prefixed **`BIZ`**.
6
+
7
+ ## Top threats
8
+
9
+ - Object and property-level authorization gaps (`BIZ-001`–`BIZ-004`, `BIZ-009`).
10
+ - Step-up auth and replay/idempotency (`BIZ-005`–`BIZ-008`, `BIZ-010`).
11
+ - SSRF and trust of internal services (`BIZ-011`, `BIZ-012`).
12
+ - Shadow APIs, exports, and webhooks (`BIZ-013`, `BIZ-016`–`BIZ-019`).
13
+
14
+ ## Pattern catalog
15
+
16
+ Complete Anti-Pattern / Safe-Pattern definitions live in [`patterns.md`](patterns.md). The table below is a **table of contents** by metric ID.
17
+
18
+ | ID | Metric | Stack |
19
+ |---|---|---|
20
+ | `BIZ-001` | BOLA: доступ к объекту по `id` без ownership check | `@app.get("/orders/{order_id}")` `async def get_order(order_id: int, user=Depends(current_user)):` ` order = await repo.get_order(order_id)` ` if not order or order.owner_id != user.id:` ` raise HTTPException(status_code=404, detail="not found")` ` return order` |
21
+ | `BIZ-002` | BOLA: доверие `user_id` из query/body | `@app.get("/profile")` `async def profile(user=Depends(current_user)):` ` return await repo.get_profile(user.id)` |
22
+ | `BIZ-003` | BOPLA: массовое обновление защищенных полей | `OWASP API Security Top 10 (2023) API3: Broken Object Property Level Authorization` |
23
+ | `BIZ-004` | Vertical privilege escalation через `role` из клиентского payload | `@app.post("/admin/promote")` `async def promote(dto: PromoteUserDTO, actor=Depends(current_user)):` ` if actor.role != "admin":` ` raise HTTPException(status_code=403, detail="forbidden")` ` if dto.role not in {"manager", "auditor"}:` ` raise HTTPException(status_code=400, detail="invalid role")` ` await repo.update_user(dto.user_id, role=dto.role)` |
24
+ | `BIZ-005` | MFA bypass: высокорисковая операция без step-up статуса | `@app.post("/payments/{payment_id}/confirm")` `async def confirm(payment_id: int, user=Depends(current_user)):` ` if not user.step_up_verified_at or user.step_up_verified_at < datetime.now(timezone.utc) - timedelta(minutes=5):` ` raise HTTPException(status_code=401, detail="step-up required")` ` return await payments.confirm(payment_id, user.id)` |
25
+ | `BIZ-006` | Missing transaction binding: подтверждение операции не связано с challenge | `@app.post("/mfa/verify")` `async def verify(req: MFAVerifyRequest, user=Depends(current_user)):` ` challenge = await mfa.get_challenge(req.challenge_id)` ` if not challenge or challenge.user_id != user.id or challenge.context != req.context:` ` raise HTTPException(status_code=400, detail="invalid challenge")` ` if not await mfa.verify_code(user.id, req.code):` ` raise HTTPException(status_code=401, detail="invalid code")` ` await mfa.mark_step_up(user.id, context=req.context)` ` return {"ok": True}` |
26
+ | `BIZ-007` | Broken workflow: пропуск обязательного шага (draft->paid напрямую) | `ALLOWED = {"draft": {"submitted"}, "submitted": {"approved"}, "approved": {"paid"}}` `@app.post("/orders/{order_id}/pay")` `async def pay(order_id: int, user=Depends(current_user)):` ` order = await repo.get_order(order_id)` ` if not order or order.owner_id != user.id:` ` raise HTTPException(status_code=404, detail="not found")` ` if "paid" not in ALLOWED.get(order.status, set()):` ` raise HTTPException(status_code=409, detail="invalid transition")` ` await repo.update_order(order_id, status="paid")` |
27
+ | `BIZ-008` | Replay на критический endpoint без idempotency key | `@app.post("/transfers")` `async def transfer(req: TransferRequest, request: Request, user=Depends(current_user)):` ` idem = request.headers.get("Idempotency-Key", "").strip()` ` if not idem:` ` raise HTTPException(status_code=400, detail="missing idempotency key")` ` if await idem_store.exists(user.id, idem):` ` raise HTTPException(status_code=409, detail="duplicate request")` ` result = await svc.transfer(user.id, req.target_id, req.amount)` ` await idem_store.save(user.id, idem)` ` return result` |
28
+ | `BIZ-009` | Tenant breakout: отсутствие tenant-scope в выборке | `@app.get("/invoices/{invoice_id}")` `async def invoice(invoice_id: int, user=Depends(current_user)):` ` inv = await repo.get_invoice(invoice_id)` ` if not inv or inv.tenant_id != user.tenant_id:` ` raise HTTPException(status_code=404, detail="not found")` ` return inv` |
29
+ | `BIZ-010` | Sensitive action без re-auth при long-lived session | `@app.post("/users/me/change-email")` `async def change_email(req: ChangeEmailRequest, user=Depends(current_user)):` ` if not await auth.verify_password(user.id, req.current_password):` ` raise HTTPException(status_code=401, detail="re-auth required")` ` await repo.update_user(user.id, email=req.new_email)` |
30
+ | `BIZ-011` | Business SSRF: сетевой вызов с необработанным пользовательским URL | `def _is_blocked_host(host: str) -> bool:` ` if host in {"localhost"}:` ` return True` ` try:` ` ip = ipaddress.ip_address(host)` ` return ip.is_private or ip.is_loopback or ip.is_link_local` ` except ValueError:` ` return False` `@app.post("/preview")` `async def preview(dto: PreviewRequest):` ` parsed = urlparse(dto.url)` ` if parsed.scheme not in {"https"} or not parsed.hostname or _is_blocked_host(parsed.hostname):` ` raise HTTPException(status_code=400, detail="blocked target")` ` async with httpx.AsyncClient(timeout=5.0, follow_redirects=False) as client:` ` r = await client.get(dto.url)` ` return {"status": r.status_code}` |
31
+ | `BIZ-012` | Insecure Internal Trust: слепое доверие данным внутреннего сервиса | `class RiskResponse(BaseModel):` ` user_id: int` ` risk_level: Literal["low", "medium", "high"]` ` allow_transfer: bool` `@app.get("/risk/{user_id}")` `async def risk(user_id: int):` ` async with httpx.AsyncClient(timeout=5.0) as client:` ` r = await client.get(f"http://risk.internal/score/{user_id}")` ` r.raise_for_status()` ` data = RiskResponse.model_validate(r.json())` ` if data.user_id != user_id:` ` raise HTTPException(status_code=502, detail="upstream mismatch")` ` return {"allow_transfer": data.allow_transfer}` |
32
+ | `BIZ-013` | Shadow API Exposure: debug/legacy endpoint активен в prod | `def create_app(env: str) -> FastAPI:` ` app = FastAPI()` ` if env != "prod":` ` @app.get("/debug/sql")` ` async def debug_sql():` ` return {"ok": True}` ` return app` `@app.get("/legacy/report", include_in_schema=False)` `async def legacy_report():` ` raise HTTPException(status_code=410, detail="endpoint removed")` |
33
+ | `BIZ-014` | Non-Atomic Financial Operations: неатомарное обновление баланса | `@app.post("/wallet/transfer")` `async def transfer(req: TransferRequest, db=Depends(get_db)):` ` async with db.transaction():` ` src = await repo.get_wallet_for_update(req.src_id, db)` ` dst = await repo.get_wallet_for_update(req.dst_id, db)` ` if src.balance < req.amount:` ` raise HTTPException(status_code=409, detail="insufficient funds")` ` await repo.update_balance(req.src_id, -req.amount, db)` ` await repo.update_balance(req.dst_id, req.amount, db)` |
34
+ | `BIZ-015` | Parameter Pollution (HPP): дубли query-параметров влияют на логику | `@app.get("/search")` `async def search(request: Request):` ` roles = request.query_params.getlist("role")` ` if len(roles) != 1:` ` raise HTTPException(status_code=400, detail="duplicate role parameter")` ` role = roles[0]` ` if role not in {"user", "manager"}:` ` raise HTTPException(status_code=400, detail="invalid role")` ` return await repo.search(role=role)` |
35
+ | `BIZ-016` | Unrestricted Export Size: экспорт без лимита количества записей | `MAX_EXPORT_ROWS = 10000` `@app.get("/exports/orders.csv")` `async def export_orders(limit: int = 1000):` ` if limit <= 0 or limit > MAX_EXPORT_ROWS:` ` raise HTTPException(status_code=400, detail="limit out of range")` ` rows = await repo.list_orders(limit=limit)` ` return to_csv(rows)` |
36
+ | `BIZ-017` | CSV/Excel Formula Injection: спецсимволы не нейтрализуются при экспорте | `DANGEROUS_PREFIXES = ("=", "+", "-", "@")` `def sanitize_cell(value: str) -> str:` ` if value.startswith(DANGEROUS_PREFIXES):` ` return "'" + value` ` return value` `def row_to_csv(user: dict[str, str]) -> list[str]:` ` return [` ` sanitize_cell(user["name"]),` ` sanitize_cell(user["email"]),` ` sanitize_cell(user["comment"]),` ` ]` |
37
+ | `BIZ-018` | Trusting Client-Side Calculations: сервер принимает цену/скидку от клиента | `@app.post("/checkout")` `async def checkout(req: CheckoutRequest, user=Depends(current_user)):` ` items = await catalog.get_items(req.item_ids)` ` subtotal = sum(item.price for item in items)` ` discount = await promotions.calculate_discount(user.id, req.promo_code, items)` ` total = max(subtotal - discount, 0)` ` if req.client_total is not None and abs(req.client_total - total) > Decimal("0.01"):` ` raise HTTPException(status_code=400, detail="price tampering detected")` ` await billing.charge(user.id, total)` |
38
+ | `BIZ-019` | Webhook Signature Verification Missing: внешние callback-и принимаются без подписи | `@app.post("/webhook/payment")` `async def payment_webhook(request: Request):` ` body = await request.body()` ` sig = request.headers.get("x-signature", "")` ` if not verify_hmac(body, sig, webhook_secret):` ` raise HTTPException(status_code=401, detail="invalid signature")` ` payload = json.loads(body)` ` ...` ` await payments.mark_paid(payload["order_id"])` |
39
+ | `BIZ-020` | RQ worker/queue without explicit safe serializer policy | Явно задавать безопасный serializer (json/msgpack), запретить pickle в job payload и валидировать схему аргументов задач. |
40
+ | `BIZ-021` | httpx call without explicit timeout (resource exhaustion risk) | Всегда задавать `timeout` (и retry budget), чтобы исключить зависание и неконтролируемое потребление ресурсов. |
41
+
42
+ ## Verification
43
+
44
+ **Verification:** Check the gold testbed file(s) below for `Vulnerable: <ID>` markers (static Semgrep + `detection-matrix.md` ground truth).
45
+
46
+ - [`gold-standard-testbed/api_vulnerable.py`](../gold-standard-testbed/api_vulnerable.py)
47
+
48
+ **Optional HTTP integration tests** (pytest + httpx; require a running API, `HEXVIBE_TARGET_URL`): [`gold-standard-testbed/integration/verify_app_logic_poc.py`](../gold-standard-testbed/integration/verify_app_logic_poc.py). See [`gold-standard-testbed/integration/README.md`](../gold-standard-testbed/integration/README.md).
49
+
50
+ After changing [`patterns.md`](patterns.md), run from the repo root:
51
+
52
+ ```bash
53
+ python scripts/sync_semgrep.py
54
+ ```
55
+
56
+ ## Workflow: Recon → Scan → Verify
57
+
58
+ ### 1) Recon
59
+ - Map entrypoints, data flows, and trust boundaries for this stack.
60
+ - Identify which metrics in [`patterns.md`](patterns.md) apply to the code under review.
61
+
62
+ ### 2) Scan
63
+ - Run Semgrep with `semgrep-rules/<skill>.yaml` (generated) and correlate with Anti-Patterns.
64
+ - Eliminate findings that cannot bind to a metric row.
65
+
66
+ ### 3) Verify
67
+ - Confirm markers or scanner hits for touched IDs in the gold testbed when adding metrics.
68
+ - Emit findings as `Vulnerable: <PREFIX>-<NNN>` in written reviews.
69
+
@@ -0,0 +1,23 @@
1
+ | ID | Название метрики | Anti-Pattern (Vulnerable Code/YAML) | Safe-Pattern (Remediation) | Stack | Источник fix_template | Exploit scenario |
2
+ |---|---|---|---|---|---|---|
3
+ | BIZ-001 | BOLA: доступ к объекту по `id` без ownership check | `@app.get("/orders/{order_id}")`<br>`async def get_order(order_id: int):`<br>` order = await repo.get_order(order_id)`<br>` return order` | `@app.get("/orders/{order_id}")`<br>`async def get_order(order_id: int, user=Depends(current_user)):`<br>` order = await repo.get_order(order_id)`<br>` if not order or order.owner_id != user.id:`<br>` raise HTTPException(status_code=404, detail="not found")`<br>` return order` | Application Logic | `OWASP API Security Top 10 (2023) API1: Broken Object Level Authorization; OWASP ASVS v4.0.3 V4 Access Control` | `@app.get("/orders/{order_id}")` `async def get_order(order_id: int, user=Depends(current_user)):` ` order = await repo.get_order(order_id)` ` if not order or order.owner_id != user.id:` ` raise HTTPException(status_code=404, detail="not found")` ` return order` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: biz-001 bola доступ к объекту по id без ownership check app get orders order async def int await repo return user -->
4
+ | BIZ-002 | BOLA: доверие `user_id` из query/body | `@app.get("/profile")`<br>`async def profile(user_id: int):`<br>` return await repo.get_profile(user_id)` | `@app.get("/profile")`<br>`async def profile(user=Depends(current_user)):`<br>` return await repo.get_profile(user.id)` | Application Logic | `OWASP API Security Top 10 (2023) API1: Broken Object Level Authorization` | `@app.get("/profile")` `async def profile(user=Depends(current_user)):` ` return await repo.get_profile(user.id)` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: biz-002 bola доверие user id из query body app get profile async def int return await repo depends current -->
5
+ | BIZ-003 | BOPLA: массовое обновление защищенных полей | `class AccountUpdate(BaseModel):`<br>` email: str`<br>` is_admin: bool = False`<br>` balance: int = 0`<br>`@app.patch("/accounts/{account_id}")`<br>`async def patch_account(account_id: int, dto: AccountUpdate):`<br>` await repo.update_account(account_id, **dto.model_dump())` | `class AccountUpdatePublic(BaseModel):`<br>` email: EmailStr | Application Logic | None = None`<br>`@app.patch("/accounts/{account_id}")`<br>`async def patch_account(account_id: int, dto: AccountUpdatePublic, user=Depends(current_user)):`<br>` account = await repo.get_account(account_id)`<br>` if not account or account.owner_id != user.id:`<br>` raise HTTPException(status_code=404, detail="not found")`<br>` payload = dto.model_dump(exclude_none=True, include={"email", "display_name"})`<br>` await repo.update_account(account_id, **payload)` | `OWASP API Security Top 10 (2023) API3: Broken Object Property Level Authorization` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: biz-003 bopla массовое обновление защищенных полей class accountupdate basemodel email str is admin bool false balance int 0 app patch accounts -->
6
+ | BIZ-004 | Vertical privilege escalation через `role` из клиентского payload | `@app.post("/admin/promote")`<br>`async def promote(dto: dict):`<br>` await repo.update_user(dto["user_id"], role=dto["role"])` | `@app.post("/admin/promote")`<br>`async def promote(dto: PromoteUserDTO, actor=Depends(current_user)):`<br>` if actor.role != "admin":`<br>` raise HTTPException(status_code=403, detail="forbidden")`<br>` if dto.role not in {"manager", "auditor"}:`<br>` raise HTTPException(status_code=400, detail="invalid role")`<br>` await repo.update_user(dto.user_id, role=dto.role)` | Application Logic | `OWASP Top 10 (2021) A01 Broken Access Control; OWASP API Security Top 10 (2023) API5 Broken Function Level Authorization` | `@app.post("/admin/promote")` `async def promote(dto: PromoteUserDTO, actor=Depends(current_user)):` ` if actor.role != "admin":` ` raise HTTPException(status_code=403, detail="forbidden")` ` if dto.role not in {"manager", "auditor"}:` ` raise HTTPException(status_code=400, detail="invalid role")` ` await repo.update_user(dto.user_id, role=dto.role)` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: biz-004 vertical privilege escalation через role из клиентского payload app post admin promote async def dto dict await repo update user -->
7
+ | BIZ-005 | MFA bypass: высокорисковая операция без step-up статуса | `@app.post("/payments/{payment_id}/confirm")`<br>`async def confirm(payment_id: int, user=Depends(current_user)):`<br>` return await payments.confirm(payment_id, user.id)` | `@app.post("/payments/{payment_id}/confirm")`<br>`async def confirm(payment_id: int, user=Depends(current_user)):`<br>` if not user.step_up_verified_at or user.step_up_verified_at < datetime.now(timezone.utc) - timedelta(minutes=5):`<br>` raise HTTPException(status_code=401, detail="step-up required")`<br>` return await payments.confirm(payment_id, user.id)` | Application Logic | `OWASP API Security Top 10 (2023) API2 Broken Authentication; OWASP ASVS v4.0.3 V2 Authentication` | `@app.post("/payments/{payment_id}/confirm")` `async def confirm(payment_id: int, user=Depends(current_user)):` ` if not user.step_up_verified_at or user.step_up_verified_at < datetime.now(timezone.utc) - timedelta(minutes=5):` ` raise HTTPException(status_code=401, detail="step-up required")` ` return await payments.confirm(payment_id, user.id)` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: biz-005 mfa bypass высокорисковая операция без step up статуса app post payments payment id confirm async def int user depends current -->
8
+ | BIZ-006 | Missing transaction binding: подтверждение операции не связано с challenge | `@app.post("/mfa/verify")`<br>`async def verify(code: str, user=Depends(current_user)):`<br>` if await mfa.verify_code(user.id, code):`<br>` return {"ok": True}` | `@app.post("/mfa/verify")`<br>`async def verify(req: MFAVerifyRequest, user=Depends(current_user)):`<br>` challenge = await mfa.get_challenge(req.challenge_id)`<br>` if not challenge or challenge.user_id != user.id or challenge.context != req.context:`<br>` raise HTTPException(status_code=400, detail="invalid challenge")`<br>` if not await mfa.verify_code(user.id, req.code):`<br>` raise HTTPException(status_code=401, detail="invalid code")`<br>` await mfa.mark_step_up(user.id, context=req.context)`<br>` return {"ok": True}` | Application Logic | `OWASP ASVS v4.0.3 V2; OWASP API Security Top 10 (2023) API2 Broken Authentication` | `@app.post("/mfa/verify")` `async def verify(req: MFAVerifyRequest, user=Depends(current_user)):` ` challenge = await mfa.get_challenge(req.challenge_id)` ` if not challenge or challenge.user_id != user.id or challenge.context != req.context:` ` raise HTTPException(status_code=400, detail="invalid challenge")` ` if not await mfa.verify_code(user.id, req.code):` ` raise HTTPException(status_code=401, detail="invalid code")` ` await mfa.mark_step_up(user.id, context=req.context)` ` return {"ok": True}` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: biz-006 missing transaction binding подтверждение операции не связано с challenge app post mfa verify async def str user depends current if -->
9
+ | BIZ-007 | Broken workflow: пропуск обязательного шага (draft->paid напрямую) | `@app.post("/orders/{order_id}/pay")`<br>`async def pay(order_id: int):`<br>` await repo.update_order(order_id, status="paid")` | `ALLOWED = {"draft": {"submitted"}, "submitted": {"approved"}, "approved": {"paid"}}`<br>`@app.post("/orders/{order_id}/pay")`<br>`async def pay(order_id: int, user=Depends(current_user)):`<br>` order = await repo.get_order(order_id)`<br>` if not order or order.owner_id != user.id:`<br>` raise HTTPException(status_code=404, detail="not found")`<br>` if "paid" not in ALLOWED.get(order.status, set()):`<br>` raise HTTPException(status_code=409, detail="invalid transition")`<br>` await repo.update_order(order_id, status="paid")` | Application Logic | `OWASP API Security Top 10 (2023) API6 Unrestricted Access to Sensitive Business Flows` | `ALLOWED = {"draft": {"submitted"}, "submitted": {"approved"}, "approved": {"paid"}}` `@app.post("/orders/{order_id}/pay")` `async def pay(order_id: int, user=Depends(current_user)):` ` order = await repo.get_order(order_id)` ` if not order or order.owner_id != user.id:` ` raise HTTPException(status_code=404, detail="not found")` ` if "paid" not in ALLOWED.get(order.status, set()):` ` raise HTTPException(status_code=409, detail="invalid transition")` ` await repo.update_order(order_id, status="paid")` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: biz-007 broken workflow пропуск обязательного шага draft paid напрямую app post orders order id pay async def int await repo update -->
10
+ | BIZ-008 | Replay на критический endpoint без idempotency key | `@app.post("/transfers")`<br>`async def transfer(req: TransferRequest, user=Depends(current_user)):`<br>` return await svc.transfer(user.id, req.target_id, req.amount)` | `@app.post("/transfers")`<br>`async def transfer(req: TransferRequest, request: Request, user=Depends(current_user)):`<br>` idem = request.headers.get("Idempotency-Key", "").strip()`<br>` if not idem:`<br>` raise HTTPException(status_code=400, detail="missing idempotency key")`<br>` if await idem_store.exists(user.id, idem):`<br>` raise HTTPException(status_code=409, detail="duplicate request")`<br>` result = await svc.transfer(user.id, req.target_id, req.amount)`<br>` await idem_store.save(user.id, idem)`<br>` return result` | Application Logic | `OWASP API Security Top 10 (2023) API6 Unrestricted Access to Sensitive Business Flows; OWASP ASVS v4.0.3 V3 Session Management` | `@app.post("/transfers")` `async def transfer(req: TransferRequest, request: Request, user=Depends(current_user)):` ` idem = request.headers.get("Idempotency-Key", "").strip()` ` if not idem:` ` raise HTTPException(status_code=400, detail="missing idempotency key")` ` if await idem_store.exists(user.id, idem):` ` raise HTTPException(status_code=409, detail="duplicate request")` ` result = await svc.transfer(user.id, req.target_id, req.amount)` ` await idem_store.save(user.id, idem)` ` return result` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: biz-008 replay на критический endpoint без idempotency key app post transfers async def transfer req transferrequest user depends current return await -->
11
+ | BIZ-009 | Tenant breakout: отсутствие tenant-scope в выборке | `@app.get("/invoices/{invoice_id}")`<br>`async def invoice(invoice_id: int):`<br>` return await repo.get_invoice(invoice_id)` | `@app.get("/invoices/{invoice_id}")`<br>`async def invoice(invoice_id: int, user=Depends(current_user)):`<br>` inv = await repo.get_invoice(invoice_id)`<br>` if not inv or inv.tenant_id != user.tenant_id:`<br>` raise HTTPException(status_code=404, detail="not found")`<br>` return inv` | Application Logic | `OWASP Top 10 (2021) A01 Broken Access Control; OWASP API Security Top 10 (2023) API1 Broken Object Level Authorization` | `@app.get("/invoices/{invoice_id}")` `async def invoice(invoice_id: int, user=Depends(current_user)):` ` inv = await repo.get_invoice(invoice_id)` ` if not inv or inv.tenant_id != user.tenant_id:` ` raise HTTPException(status_code=404, detail="not found")` ` return inv` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: biz-009 tenant breakout отсутствие scope в выборке app get invoices invoice id async def int return await repo user depends current -->
12
+ | BIZ-010 | Sensitive action без re-auth при long-lived session | `@app.post("/users/me/change-email")`<br>`async def change_email(req: ChangeEmailRequest, user=Depends(current_user)):`<br>` await repo.update_user(user.id, email=req.new_email)` | `@app.post("/users/me/change-email")`<br>`async def change_email(req: ChangeEmailRequest, user=Depends(current_user)):`<br>` if not await auth.verify_password(user.id, req.current_password):`<br>` raise HTTPException(status_code=401, detail="re-auth required")`<br>` await repo.update_user(user.id, email=req.new_email)` | Application Logic | `OWASP API Security Top 10 (2023) API2 Broken Authentication; OWASP ASVS v4.0.3 V2` | `@app.post("/users/me/change-email")` `async def change_email(req: ChangeEmailRequest, user=Depends(current_user)):` ` if not await auth.verify_password(user.id, req.current_password):` ` raise HTTPException(status_code=401, detail="re-auth required")` ` await repo.update_user(user.id, email=req.new_email)` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: biz-010 sensitive action без re auth при long lived session app post users me change email async def req changeemailrequest user -->
13
+ | BIZ-011 | Business SSRF: сетевой вызов с необработанным пользовательским URL | `@app.post("/preview")`<br>`async def preview(dto: dict):`<br>` async with httpx.AsyncClient() as client:`<br>` r = await client.get(dto["url"])`<br>` return {"status": r.status_code}` | `def _is_blocked_host(host: str) -> bool:`<br>` if host in {"localhost"}:`<br>` return True`<br>` try:`<br>` ip = ipaddress.ip_address(host)`<br>` return ip.is_private or ip.is_loopback or ip.is_link_local`<br>` except ValueError:`<br>` return False`<br>`@app.post("/preview")`<br>`async def preview(dto: PreviewRequest):`<br>` parsed = urlparse(dto.url)`<br>` if parsed.scheme not in {"https"} or not parsed.hostname or _is_blocked_host(parsed.hostname):`<br>` raise HTTPException(status_code=400, detail="blocked target")`<br>` async with httpx.AsyncClient(timeout=5.0, follow_redirects=False) as client:`<br>` r = await client.get(dto.url)`<br>` return {"status": r.status_code}` | Application Logic | `OWASP API Security Top 10 (2023) API7: Server Side Request Forgery; OWASP ASVS v4.0.3 V5 Validation, Sanitization and Encoding` | `def _is_blocked_host(host: str) -> bool:` ` if host in {"localhost"}:` ` return True` ` try:` ` ip = ipaddress.ip_address(host)` ` return ip.is_private or ip.is_loopback or ip.is_link_local` ` except ValueError:` ` return False` `@app.post("/preview")` `async def preview(dto: PreviewRequest):` ` parsed = urlparse(dto.url)` ` if parsed.scheme not in {"https"} or not parsed.hostname or _is_blocked_host(parsed.hostname):` ` raise HTTPException(status_code=400, detail="blocked target")` ` async with httpx.AsyncClient(timeout=5.0, follow_redirects=False) as client:` ` r = await client.get(dto.url)` ` return {"status": r.status_code}` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: biz-011 business ssrf сетевой вызов с необработанным пользовательским url app post preview async def dto dict with httpx asyncclient as client -->
14
+ | BIZ-012 | Insecure Internal Trust: слепое доверие данным внутреннего сервиса | `@app.get("/risk/{user_id}")`<br>`async def risk(user_id: int):`<br>` async with httpx.AsyncClient(timeout=5.0) as client:`<br>` r = await client.get(f"http://risk.internal/score/{user_id}")`<br>` return {"allow_transfer": r.json()["allow_transfer"]}` | `class RiskResponse(BaseModel):`<br>` user_id: int`<br>` risk_level: Literal["low", "medium", "high"]`<br>` allow_transfer: bool`<br>`@app.get("/risk/{user_id}")`<br>`async def risk(user_id: int):`<br>` async with httpx.AsyncClient(timeout=5.0) as client:`<br>` r = await client.get(f"http://risk.internal/score/{user_id}")`<br>` r.raise_for_status()`<br>` data = RiskResponse.model_validate(r.json())`<br>` if data.user_id != user_id:`<br>` raise HTTPException(status_code=502, detail="upstream mismatch")`<br>` return {"allow_transfer": data.allow_transfer}` | Application Logic | `OWASP API Security Top 10 (2023) API10: Unsafe Consumption of APIs; OWASP ASVS v4.0.3 V14 Config / V5 Input Validation` | `class RiskResponse(BaseModel):` ` user_id: int` ` risk_level: Literal["low", "medium", "high"]` ` allow_transfer: bool` `@app.get("/risk/{user_id}")` `async def risk(user_id: int):` ` async with httpx.AsyncClient(timeout=5.0) as client:` ` r = await client.get(f"http://risk.internal/score/{user_id}")` ` r.raise_for_status()` ` data = RiskResponse.model_validate(r.json())` ` if data.user_id != user_id:` ` raise HTTPException(status_code=502, detail="upstream mismatch")` ` return {"allow_transfer": data.allow_transfer}` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: biz-012 insecure internal trust слепое доверие данным внутреннего сервиса app get risk user id async def int with httpx asyncclient timeout -->
15
+ | BIZ-013 | Shadow API Exposure: debug/legacy endpoint активен в prod | `@app.get("/debug/sql")`<br>`async def debug_sql():`<br>` return {"dsn": os.getenv("DATABASE_URL")}` | `def create_app(env: str) -> FastAPI:`<br>` app = FastAPI()`<br>` if env != "prod":`<br>` @app.get("/debug/sql")`<br>` async def debug_sql():`<br>` return {"ok": True}`<br>` return app`<br>`@app.get("/legacy/report", include_in_schema=False)`<br>`async def legacy_report():`<br>` raise HTTPException(status_code=410, detail="endpoint removed")` | Application Logic | `OWASP API Security Top 10 (2023) API9: Improper Inventory Management; OWASP Top 10 (2021) A05 Security Misconfiguration` | `def create_app(env: str) -> FastAPI:` ` app = FastAPI()` ` if env != "prod":` ` @app.get("/debug/sql")` ` async def debug_sql():` ` return {"ok": True}` ` return app` `@app.get("/legacy/report", include_in_schema=False)` `async def legacy_report():` ` raise HTTPException(status_code=410, detail="endpoint removed")` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: biz-013 shadow api exposure debug legacy endpoint активен в prod app get sql async def return dsn os getenv database url -->
16
+ | BIZ-014 | Non-Atomic Financial Operations: неатомарное обновление баланса | `@app.post("/wallet/transfer")`<br>`async def transfer(req: TransferRequest):`<br>` src = await repo.get_wallet(req.src_id)`<br>` dst = await repo.get_wallet(req.dst_id)`<br>` src.balance -= req.amount`<br>` dst.balance += req.amount`<br>` await repo.save_wallet(src)`<br>` await repo.save_wallet(dst)` | `@app.post("/wallet/transfer")`<br>`async def transfer(req: TransferRequest, db=Depends(get_db)):`<br>` async with db.transaction():`<br>` src = await repo.get_wallet_for_update(req.src_id, db)`<br>` dst = await repo.get_wallet_for_update(req.dst_id, db)`<br>` if src.balance < req.amount:`<br>` raise HTTPException(status_code=409, detail="insufficient funds")`<br>` await repo.update_balance(req.src_id, -req.amount, db)`<br>` await repo.update_balance(req.dst_id, req.amount, db)` | Application Logic | `OWASP API Security Top 10 (2023) API6: Unrestricted Access to Sensitive Business Flows; OWASP Top 10 (2021) A04 Insecure Design` | `@app.post("/wallet/transfer")` `async def transfer(req: TransferRequest, db=Depends(get_db)):` ` async with db.transaction():` ` src = await repo.get_wallet_for_update(req.src_id, db)` ` dst = await repo.get_wallet_for_update(req.dst_id, db)` ` if src.balance < req.amount:` ` raise HTTPException(status_code=409, detail="insufficient funds")` ` await repo.update_balance(req.src_id, -req.amount, db)` ` await repo.update_balance(req.dst_id, req.amount, db)` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: biz-014 non atomic financial operations неатомарное обновление баланса app post wallet transfer async def req transferrequest src await repo get id -->
17
+ | BIZ-015 | Parameter Pollution (HPP): дубли query-параметров влияют на логику | `@app.get("/search")`<br>`async def search(role: str = "user"):`<br>` return await repo.search(role=role)` | `@app.get("/search")`<br>`async def search(request: Request):`<br>` roles = request.query_params.getlist("role")`<br>` if len(roles) != 1:`<br>` raise HTTPException(status_code=400, detail="duplicate role parameter")`<br>` role = roles[0]`<br>` if role not in {"user", "manager"}:`<br>` raise HTTPException(status_code=400, detail="invalid role")`<br>` return await repo.search(role=role)` | Application Logic | `OWASP API Security Top 10 (2023) API8: Security Misconfiguration; OWASP ASVS v4.0.3 V5 Validation, Sanitization and Encoding` | `@app.get("/search")` `async def search(request: Request):` ` roles = request.query_params.getlist("role")` ` if len(roles) != 1:` ` raise HTTPException(status_code=400, detail="duplicate role parameter")` ` role = roles[0]` ` if role not in {"user", "manager"}:` ` raise HTTPException(status_code=400, detail="invalid role")` ` return await repo.search(role=role)` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: biz-015 parameter pollution hpp дубли query параметров влияют на логику app get search async def role str user return await repo -->
18
+ | BIZ-016 | Unrestricted Export Size: экспорт без лимита количества записей | `@app.get("/exports/orders.csv")`<br>`async def export_orders(limit: int = 1000000):`<br>` rows = await repo.list_orders(limit=limit)`<br>` return to_csv(rows)` | `MAX_EXPORT_ROWS = 10000`<br>`@app.get("/exports/orders.csv")`<br>`async def export_orders(limit: int = 1000):`<br>` if limit <= 0 or limit > MAX_EXPORT_ROWS:`<br>` raise HTTPException(status_code=400, detail="limit out of range")`<br>` rows = await repo.list_orders(limit=limit)`<br>` return to_csv(rows)` | Application Logic | `OWASP API Security Top 10 (2023) API4: Unrestricted Resource Consumption; OWASP ASVS v4.0.3 V1 Architecture, Design and Threat Modeling` | `MAX_EXPORT_ROWS = 10000` `@app.get("/exports/orders.csv")` `async def export_orders(limit: int = 1000):` ` if limit <= 0 or limit > MAX_EXPORT_ROWS:` ` raise HTTPException(status_code=400, detail="limit out of range")` ` rows = await repo.list_orders(limit=limit)` ` return to_csv(rows)` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: biz-016 unrestricted export size экспорт без лимита количества записей app get exports orders csv async def limit int 1000000 rows await -->
19
+ | BIZ-017 | CSV/Excel Formula Injection: спецсимволы не нейтрализуются при экспорте | `def row_to_csv(user: dict[str, str]) -> list[str]:`<br>` return [user["name"], user["email"], user["comment"]]` | `DANGEROUS_PREFIXES = ("=", "+", "-", "@")`<br>`def sanitize_cell(value: str) -> str:`<br>` if value.startswith(DANGEROUS_PREFIXES):`<br>` return "'" + value`<br>` return value`<br>`def row_to_csv(user: dict[str, str]) -> list[str]:`<br>` return [`<br>` sanitize_cell(user["name"]),`<br>` sanitize_cell(user["email"]),`<br>` sanitize_cell(user["comment"]),`<br>` ]` | Application Logic | `OWASP API Security Top 10 (2023) API8: Security Misconfiguration; OWASP ASVS v4.0.3 V5 Validation, Sanitization and Encoding` | `DANGEROUS_PREFIXES = ("=", "+", "-", "@")` `def sanitize_cell(value: str) -> str:` ` if value.startswith(DANGEROUS_PREFIXES):` ` return "'" + value` ` return value` `def row_to_csv(user: dict[str, str]) -> list[str]:` ` return [` ` sanitize_cell(user["name"]),` ` sanitize_cell(user["email"]),` ` sanitize_cell(user["comment"]),` ` ]` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: biz-017 csv excel formula injection спецсимволы не нейтрализуются при экспорте def row to user dict str list return name email comment -->
20
+ | BIZ-018 | Trusting Client-Side Calculations: сервер принимает цену/скидку от клиента | `@app.post("/checkout")`<br>`async def checkout(payload: dict):`<br>` order_total = payload["client_total"]`<br>` discount = payload.get("discount", 0)`<br>` await billing.charge(payload["user_id"], order_total - discount)` | `@app.post("/checkout")`<br>`async def checkout(req: CheckoutRequest, user=Depends(current_user)):`<br>` items = await catalog.get_items(req.item_ids)`<br>` subtotal = sum(item.price for item in items)`<br>` discount = await promotions.calculate_discount(user.id, req.promo_code, items)`<br>` total = max(subtotal - discount, 0)`<br>` if req.client_total is not None and abs(req.client_total - total) > Decimal("0.01"):`<br>` raise HTTPException(status_code=400, detail="price tampering detected")`<br>` await billing.charge(user.id, total)` | Application Logic | `OWASP API Security Top 10 (2023) API3: Broken Object Property Level Authorization / API8: Security Misconfiguration; OWASP ASVS v4.0.3 V4 Access Control and V5 Validation` | `@app.post("/checkout")` `async def checkout(req: CheckoutRequest, user=Depends(current_user)):` ` items = await catalog.get_items(req.item_ids)` ` subtotal = sum(item.price for item in items)` ` discount = await promotions.calculate_discount(user.id, req.promo_code, items)` ` total = max(subtotal - discount, 0)` ` if req.client_total is not None and abs(req.client_total - total) > Decimal("0.01"):` ` raise HTTPException(status_code=400, detail="price tampering detected")` ` await billing.charge(user.id, total)` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: biz-018 trusting client side calculations сервер принимает цену скидку от клиента app post checkout async def payload dict order total discount -->
21
+ | BIZ-019 | Webhook Signature Verification Missing: внешние callback-и принимаются без подписи | `@app.post("/webhook/payment")`<br>`async def payment_webhook(request: Request):`<br>` payload = await request.json()`<br>` ...`<br>` await payments.mark_paid(payload["order_id"])` | `@app.post("/webhook/payment")`<br>`async def payment_webhook(request: Request):`<br>` body = await request.body()`<br>` sig = request.headers.get("x-signature", "")`<br>` if not verify_hmac(body, sig, webhook_secret):`<br>` raise HTTPException(status_code=401, detail="invalid signature")`<br>` payload = json.loads(body)`<br>` ...`<br>` await payments.mark_paid(payload["order_id"])` | Application Logic | `CWE-345` | `@app.post("/webhook/payment")` `async def payment_webhook(request: Request):` ` body = await request.body()` ` sig = request.headers.get("x-signature", "")` ` if not verify_hmac(body, sig, webhook_secret):` ` raise HTTPException(status_code=401, detail="invalid signature")` ` payload = json.loads(body)` ` ...` ` await payments.mark_paid(payload["order_id"])` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: biz-019 webhook signature verification missing внешние callback и принимаются без подписи app post payment async def request payload await json payments -->
22
+ | BIZ-020 | RQ worker/queue without explicit safe serializer policy | `Queue("default", connection=redis_conn)`<br>`Worker([q], connection=redis_conn)` | Явно задавать безопасный serializer (json/msgpack), запретить pickle в job payload и валидировать схему аргументов задач. | Application Logic | CWE Final Certification | Явно задавать безопасный serializer (json/msgpack), запретить pickle в job payload и валидировать схему аргументов задач. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. |
23
+ | BIZ-021 | httpx call without explicit timeout (resource exhaustion risk) | `httpx.get(url)`<br>`httpx.post(api, json=payload)` | Всегда задавать `timeout` (и retry budget), чтобы исключить зависание и неконтролируемое потребление ресурсов. | Application Logic | CWE Final Certification | Всегда задавать `timeout` (и retry budget), чтобы исключить зависание и неконтролируемое потребление ресурсов. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. |
@@ -0,0 +1,24 @@
1
+ {
2
+ "skill_id": "app-logic",
3
+ "name": "Application Business Logic",
4
+ "activation_triggers": [
5
+ "biz-bola-object-id",
6
+ "biz-bopla-mass-assign",
7
+ "biz-idempotency-key",
8
+ "biz-sensitive-webhook",
9
+ "biz-workflow-transition"
10
+ ],
11
+ "relevant_extensions": [
12
+ ".py",
13
+ ".ts",
14
+ ".js"
15
+ ],
16
+ "tools": [
17
+ "semgrep",
18
+ "syft",
19
+ "trufflehog"
20
+ ],
21
+ "rules_path": "core/skills/app-logic/patterns.md",
22
+ "few_shot_examples": "core/gold-standard-testbed/api_vulnerable.py",
23
+ "security_priority": 5
24
+ }
@@ -0,0 +1,69 @@
1
+ # Auth / Keycloak / OIDC
2
+
3
+ ## Stack overview
4
+
5
+ **OAuth2/OIDC** clients, **JWT** validation, **Keycloak 22/24** integration, token exchange, and browser-flow hardening. Metrics are prefixed **`AK`**.
6
+
7
+ ## Top threats
8
+
9
+ - Algorithm confusion and weak JWT validation (`AK-001`, `AK-002`, `AK-008`, `AK-016`).
10
+ - Redirect and session fixation (`AK-004`, `AK-006`, `AK-015`, `AK-016`).
11
+ - Secret handling and token forwarding (`AK-005`, `AK-007`, `AK-011`–`AK-014`).
12
+ - PKCE, DPoP, and operational abuse (`AK-009`, `AK-010`, `AK-012`).
13
+
14
+ ## Pattern catalog
15
+
16
+ Complete Anti-Pattern / Safe-Pattern definitions live in [`patterns.md`](patterns.md). The table below is a **table of contents** by metric ID.
17
+
18
+ | ID | Metric | Stack |
19
+ |---|---|---|
20
+ | `AK-001` | Weak Algorithm: разрешен `alg=none` или нефиксированный алгоритм | `from jose import jwt` `header = jwt.get_unverified_header(token)` `if header.get(\"alg\") not in {\"RS256\", \"ES256\", \"GOST3410\"}:` ` raise ValueError(\"unsupported alg\")` `claims = jwt.decode(` ` token,` ` jwk,` ` algorithms=[\"RS256\", \"ES256\", \"GOST3410\"],` ` issuer=issuer_url,` ` audience=client_id,` ` options={\"verify_signature\": True},` `)` `# для контура Клинкера включить профиль российских криптоалгоритмов (ГОСТ)` |
21
+ | `AK-002` | Issuer/Audience Mismatch: невалидируемые `iss` и `aud` | `import jwt` `claims = jwt.decode(` ` token,` ` pub_key,` ` algorithms=[\"RS256\", \"ES256\"],` ` issuer=issuer_url,` ` audience=client_id,` ` options={\"verify_signature\": True, \"verify_exp\": True, \"verify_nbf\": True, \"verify_iat\": True},` `)` |
22
+ | `AK-003` | JWS Header Injection: прямое доверие `kid` из заголовка | `header = jwt.get_unverified_header(token)` `kid = header.get(\"kid\")` `trusted_kids = {k[\"kid\"] for k in jwks[\"keys\"]}` `if kid not in trusted_kids:` ` raise ValueError(\"untrusted kid\")` `jwk = next(k for k in jwks[\"keys\"] if k[\"kid\"] == kid)` `claims = jwt.decode(token, jwk, algorithms=[\"RS256\", \"ES256\"], issuer=issuer_url, audience=client_id)` |
23
+ | `AK-004` | Insecure Redirects: wildcard и нет точного HTTPS-match | `allowed_redirects = {` ` \"https://app.example.com/oidc/callback\",` ` \"https://admin.example.com/oidc/callback\",` `}` `if redirect_uri not in allowed_redirects:` ` raise ValueError(\"redirect_uri mismatch\")` |
24
+ | `AK-005` | Client Secret Exposure: secret захардкожен в коде | `import os` `from keycloak import KeycloakOpenID` `kc = KeycloakOpenID(` ` server_url=os.environ[\"KEYCLOAK_URL\"],` ` realm_name=os.environ[\"KEYCLOAK_REALM\"],` ` client_id=os.environ[\"KEYCLOAK_CLIENT_ID\"],` ` client_secret_key=os.environ[\"KEYCLOAK_CLIENT_SECRET\"],` `)` |
25
+ | `AK-006` | Subject Confusion: `sub` не связан с текущим пользователем | `claims = jwt.decode(token, jwk, algorithms=[\"RS256\", \"ES256\"], issuer=issuer_url, audience=client_id, options={\"verify_exp\": True, \"verify_nbf\": True, \"verify_iat\": True})` `user = db.get_user_by_id(current_user_id)` `if claims.get(\"sub\") != user.oidc_sub:` ` raise ValueError(\"subject mismatch\")` |
26
+ | `AK-007` | Authorization Code не привязан к `redirect_uri` и `client_id` | `assert request_client_id == stored_client_id_for_code(code)` `assert request_redirect_uri == stored_redirect_uri_for_code(code)` `token = exchange_code_for_token(code=code, client_id=request_client_id, redirect_uri=request_redirect_uri)` |
27
+ | `AK-008` | Нет обязательной проверки времени жизни токена (`exp/nbf/iat`) | `claims = jwt.decode(token, jwk, algorithms=[\"RS256\", \"ES256\"], issuer=issuer_url, audience=client_id, options={\"verify_exp\": True, \"verify_nbf\": True, \"verify_iat\": True})` |
28
+ | `AK-009` | PKCE Enforcement: Authorization Code Flow без `code_challenge`/`code_verifier` | `auth_url = f"{issuer}/protocol/openid-connect/auth?response_type=code&client_id={client_id}&redirect_uri={redirect_uri}&code_challenge={code_challenge}&code_challenge_method=S256"` `token = exchange_code(code=code, code_verifier=code_verifier)` `if not code_verifier:` ` raise ValueError("pkce required")` |
29
+ | `AK-010` | DPoP отсутствует для высокорисковых операций (Token Theft risk) | `def call_high_risk_api(access_token: str, dpop_proof: str):` ` if not dpop_proof:` ` raise ValueError("DPoP proof required")` ` return client.post("/payments/transfer", headers={"Authorization": f"Bearer {access_token}", "DPoP": dpop_proof})` `# DPoP обязателен для высокорисковых операций ЦБ, чтобы снизить риск кражи токенов` |
30
+ | `AK-011` | PII in JWT: конфиденциальные данные в открытом payload | `payload = {"sub": user_id, "role": role, "scope": "api.read"}` `token = jwt.encode(payload, private_key, algorithm="RS256")` `# PII moved to userinfo endpoint or encrypted storage` |
31
+ | `AK-012` | JWKS Rate Limiting: нет ограничений на запросы к `/.well-known/jwks.json` при неизвестн... | `def get_jwk_for_kid(kid: str):` ` if kid in negative_kid_cache and not negative_kid_cache[kid].expired:` ` raise ValueError("unknown kid cached")` ` if not jwks_rate_limiter.allow("jwks_fetch"):` ` raise RuntimeError("jwks rate limit exceeded")` ` jwks = requests.get(f"{issuer}/.well-known/jwks.json", timeout=2).json()` ` # cache keys and unknown kid misses` ` return select_key_from_jwks(jwks, kid)` |
32
+ | `AK-013` | Insecure Token Forwarding: прямой проброс пользовательского JWT между микросервисами | `def exchange_token(user_jwt: str, audience: str) -> str:` ` resp = requests.post(f"{issuer}/protocol/openid-connect/token", data={` ` "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",` ` "subject_token": user_jwt,` ` "subject_token_type": "urn:ietf:params:oauth:token-type:access_token",` ` "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",` ` "audience": audience,` ` }, auth=(client_id, client_secret), cert=(client_cert_path, client_key_path), timeout=5)` ` resp.raise_for_status()` ` return resp.json()["access_token"]` `def call_internal_service(user_jwt: str):` ` svc_token = exchange_token(user_jwt, audience="orders-api")` ` return requests.get("http://orders.internal/api/orders", headers={"Authorization": f"Bearer {svc_token}"}, timeout=5)` `# token exchange endpoint /token вызывать только по mTLS в профиле ФАПИ.ПАОК` |
33
+ | `AK-014` | Missing Resource Indicators: отсутствует параметр `resource` при запросе токена | `token_req = {` ` "grant_type": "authorization_code",` ` "code": code,` ` "redirect_uri": redirect_uri,` ` "resource": "https://api.example.com/orders",` `}` `token = requests.post(token_url, data=token_req, auth=(client_id, client_secret), timeout=5)` `token.raise_for_status()` |
34
+ | `AK-015` | OIDC State Validation Missing: callback не проверяет `state` | `@app.get("/oidc/callback")` `async def callback(code: str, state: str, request: Request):` ` expected = request.session.get("oidc_state")` ` if not expected or state != expected:` ` raise HTTPException(status_code=401, detail="invalid state")` ` ...` ` return await exchange_code(code)` |
35
+ | `AK-016` | OIDC Nonce Validation Missing: ID Token принимается без проверки `nonce` | `id_claims = jwt.decode(id_token, jwk, algorithms=["RS256","ES256"], audience=client_id, issuer=issuer)` `expected_nonce = request.session.get("oidc_nonce")` `if not expected_nonce or id_claims.get("nonce") != expected_nonce:` ` raise HTTPException(status_code=401, detail="invalid nonce")` `...` `return id_claims` |
36
+ | `AK-017` | Session Management: нет принудительного logout и refresh_token TTL > 24ч | `refresh_token_ttl = 86400` `if refresh_token_ttl > 86400:` ` raise ValueError("CB session limit exceeded")` `enable_backchannel_logout = True` `enable_frontchannel_logout = True` `revoke_refresh_token_on_logout = True` |
37
+ | `AK-018` | Zero Trust mTLS: межсервисные вызовы выполняются без mTLS | Все межсервисные вызовы выполнять по mTLS (service identity, cert pinning, trust policy), не только token exchange endpoint. |
38
+ | `AK-019` | ASVS L3 Admin Session: отсутствует ротация секретов и ограничение админ-сессий | Для админ-учетных записей принудительная ротация клиентских секретов, короткий TTL сессий, step-up auth и немедленный revoke при logout/risk events. |
39
+ | `AK-020` | jose.jwt.decode with `verify_signature=False` | Никогда не отключать проверку подписи; валидировать подпись JWT по JWKS и reject токены с invalid signature. |
40
+ | `AK-021` | jose.jwt.decode without explicit algorithms allowlist | Всегда указывать `algorithms=["RS256"]` (или строгий allowlist) и запрещать algorithm confusion/fallback. |
41
+
42
+ ## Verification
43
+
44
+ **Verification:** Check the gold testbed file(s) below for `Vulnerable: <ID>` markers (static Semgrep + `detection-matrix.md` ground truth).
45
+
46
+ - [`gold-standard-testbed/api_vulnerable.py`](../gold-standard-testbed/api_vulnerable.py)
47
+
48
+ **Optional HTTP integration tests** (pytest + httpx; require a running API, `HEXVIBE_TARGET_URL`): [`gold-standard-testbed/integration/verify_auth_keycloak_poc.py`](../gold-standard-testbed/integration/verify_auth_keycloak_poc.py). See [`gold-standard-testbed/integration/README.md`](../gold-standard-testbed/integration/README.md).
49
+
50
+ After changing [`patterns.md`](patterns.md), run from the repo root:
51
+
52
+ ```bash
53
+ python scripts/sync_semgrep.py
54
+ ```
55
+
56
+ ## Workflow: Recon → Scan → Verify
57
+
58
+ ### 1) Recon
59
+ - Map entrypoints, data flows, and trust boundaries for this stack.
60
+ - Identify which metrics in [`patterns.md`](patterns.md) apply to the code under review.
61
+
62
+ ### 2) Scan
63
+ - Run Semgrep with `semgrep-rules/<skill>.yaml` (generated) and correlate with Anti-Patterns.
64
+ - Eliminate findings that cannot bind to a metric row.
65
+
66
+ ### 3) Verify
67
+ - Confirm markers or scanner hits for touched IDs in the gold testbed when adding metrics.
68
+ - Emit findings as `Vulnerable: <PREFIX>-<NNN>` in written reviews.
69
+