@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,46 @@
1
+ | ID | Название метрики | Anti-Pattern (Vulnerable Code/YAML) | Safe-Pattern (Remediation) | Stack | Источник fix_template | Exploit scenario |
2
+ |---|---|---|---|---|---|---|
3
+ | AK-001 | Weak Algorithm: разрешен `alg=none` или нефиксированный алгоритм | `from jose import jwt`<br>`claims = jwt.decode(token, key=\"\", algorithms=[\"none\"])`<br>`# либо library defaults без allowlist alg` | `from jose import jwt`<br>`header = jwt.get_unverified_header(token)`<br>`if header.get(\"alg\") not in {\"RS256\", \"ES256\", \"GOST3410\"}:`<br>` raise ValueError(\"unsupported alg\")`<br>`claims = jwt.decode(`<br>` token,`<br>` jwk,`<br>` algorithms=[\"RS256\", \"ES256\", \"GOST3410\"],`<br>` issuer=issuer_url,`<br>` audience=client_id,`<br>` options={\"verify_signature\": True},`<br>`)`<br>`# для контура Клинкера включить профиль российских криптоалгоритмов (ГОСТ)` | Identity/OIDC | `Auth0 JWT Handbook, Validate JSON Web Tokens > Manually implement checks (disallow 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},` `)` `# для контура Клинкера включить профиль российских криптоалгоритмов (ГОСТ)` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: ak-001 weak algorithm gost3410 klinker jwt jws allowlist rs256 es256 российские криптоалгоритмы -->
4
+ | AK-002 | Issuer/Audience Mismatch: невалидируемые `iss` и `aud` | `import jwt`<br>`claims = jwt.decode(token, pub_key, algorithms=[\"RS256\"], options={\"verify_signature\": True, \"verify_exp\": True})` | `import jwt`<br>`claims = jwt.decode(`<br>` token,`<br>` pub_key,`<br>` algorithms=[\"RS256\", \"ES256\"],`<br>` issuer=issuer_url,`<br>` audience=client_id,`<br>` options={\"verify_signature\": True, \"verify_exp\": True, \"verify_nbf\": True, \"verify_iat\": True},`<br>`)` | Identity/OIDC | `Auth0 JWT Handbook, Validate JSON Web Tokens > Issuer Validation + Audience Validation` | `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},` `)` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: ak-002 issuer audience mismatch невалидируемые iss и aud import jwt claims decode token pub key algorithms rs256 options verify signature true -->
5
+ | AK-003 | JWS Header Injection: прямое доверие `kid` из заголовка | `header = jwt.get_unverified_header(token)`<br>`jwk = jwks[header[\"kid\"]]`<br>`claims = jwt.decode(token, jwk, algorithms=[\"RS256\"])` | `header = jwt.get_unverified_header(token)`<br>`kid = header.get(\"kid\")`<br>`trusted_kids = {k[\"kid\"] for k in jwks[\"keys\"]}`<br>`if kid not in trusted_kids:`<br>` raise ValueError(\"untrusted kid\")`<br>`jwk = next(k for k in jwks[\"keys\"] if k[\"kid\"] == kid)`<br>`claims = jwt.decode(token, jwk, algorithms=[\"RS256\", \"ES256\"], issuer=issuer_url, audience=client_id)` | Identity/OIDC | `Auth0 JWT Handbook, Validate JSON Web Tokens > Key ID (kid) Header and JWKS Matching` | `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)` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: ak-003 jws header injection прямое доверие kid из заголовка jwt get unverified token jwk jwks claims decode algorithms rs256 trusted kids -->
6
+ | AK-004 | Insecure Redirects: wildcard и нет точного HTTPS-match | `allowed_redirects = [\"https://app.example.com/*\", \"http://localhost/*\"]`<br>`if redirect_uri.startswith(\"https://app.example.com/\"):`<br>` pass` | `allowed_redirects = {`<br>` \"https://app.example.com/oidc/callback\",`<br>` \"https://admin.example.com/oidc/callback\",`<br>`}`<br>`if redirect_uri not in allowed_redirects:`<br>` raise ValueError(\"redirect_uri mismatch\")` | Identity/OIDC | `RFC 6819, section 5.2.3.5` | `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\")` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: ak-004 insecure redirects wildcard и нет точного https match allowed app example com http localhost if redirect uri startswith pass oidc -->
7
+ | AK-005 | Client Secret Exposure: secret захардкожен в коде | `from keycloak import KeycloakOpenID`<br>`kc = KeycloakOpenID(`<br>` server_url=\"https://id.example.com\",`<br>` realm_name=\"prod\",`<br>` client_id=\"backend\",`<br>` client_secret_key=\"hardcoded-secret\",`<br>`)` | `import os`<br>`from keycloak import KeycloakOpenID`<br>`kc = KeycloakOpenID(`<br>` server_url=os.environ[\"KEYCLOAK_URL\"],`<br>` realm_name=os.environ[\"KEYCLOAK_REALM\"],`<br>` client_id=os.environ[\"KEYCLOAK_CLIENT_ID\"],`<br>` client_secret_key=os.environ[\"KEYCLOAK_CLIENT_SECRET\"],`<br>`)` | Identity/OIDC | `RFC 6819, section 5.2.3.6; OAuth 2.0 (RFC 6749), section 10.1` | `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\"],` `)` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: ak-005 client secret exposure захардкожен в коде from keycloak import keycloakopenid kc server url https id example com realm name prod -->
8
+ | AK-006 | Subject Confusion: `sub` не связан с текущим пользователем | `claims = jwt.decode(token, jwk, algorithms=[\"RS256\"], issuer=issuer_url, audience=client_id)`<br>`user = db.get_user_by_email(claims.get(\"email\"))`<br>`# 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})`<br>`user = db.get_user_by_id(current_user_id)`<br>`if claims.get(\"sub\") != user.oidc_sub:`<br>` raise ValueError(\"subject mismatch\")` | Identity/OIDC | `Auth0 JWT Handbook, Validate JSON Web Tokens > Validate claims` | `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\")` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: ak-006 subject confusion sub не связан с текущим пользователем claims jwt decode token jwk algorithms rs256 issuer url audience client id -->
9
+ | AK-007 | Authorization Code не привязан к `redirect_uri` и `client_id` | `token = exchange_code_for_token(code=code, client_id=client_id)` | `assert request_client_id == stored_client_id_for_code(code)`<br>`assert request_redirect_uri == stored_redirect_uri_for_code(code)`<br>`token = exchange_code_for_token(code=code, client_id=request_client_id, redirect_uri=request_redirect_uri)` | Identity/OIDC | `RFC 6819, section 5.2.4.5` | `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)` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: ak-007 authorization не привязан к redirect uri и client id token exchange for assert request stored -->
10
+ | AK-008 | Нет обязательной проверки времени жизни токена (`exp/nbf/iat`) | `claims = jwt.decode(token, jwk, algorithms=[\"RS256\"], issuer=issuer_url, audience=client_id, options={\"verify_exp\": False, \"verify_nbf\": False, \"verify_iat\": False})` | `claims = jwt.decode(token, jwk, algorithms=[\"RS256\", \"ES256\"], issuer=issuer_url, audience=client_id, options={\"verify_exp\": True, \"verify_nbf\": True, \"verify_iat\": True})` | Identity/OIDC | `Auth0 JWT Handbook, Validate JSON Web Tokens > JWT validation checks structure, claims, and signature` | `claims = jwt.decode(token, jwk, algorithms=[\"RS256\", \"ES256\"], issuer=issuer_url, audience=client_id, options={\"verify_exp\": True, \"verify_nbf\": True, \"verify_iat\": True})` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: ak-008 нет обязательной проверки времени жизни токена exp nbf iat claims jwt decode token jwk algorithms rs256 issuer url audience client -->
11
+ | 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}"`<br>`# token exchange without code_verifier`<br>`token = exchange_code(code=code)` | `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"`<br>`token = exchange_code(code=code, code_verifier=code_verifier)`<br>`if not code_verifier:`<br>` raise ValueError("pkce required")` | Identity/OIDC | `FAPI 2.0 Security Profile; ГОСТ 57580.1 (IA); OAuth 2.1 Draft; OWASP ASVS v4.0.3` | `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")` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: ak-009 pkce enforcement fapi 2 0 gost 57580 ia code flow verifier challenge -->
12
+ | AK-010 | DPoP отсутствует для высокорисковых операций (Token Theft risk) | `def call_high_risk_api(access_token: str):`<br>` return client.post("/payments/transfer", headers={"Authorization": f"Bearer {access_token}"})` | `def call_high_risk_api(access_token: str, dpop_proof: str):`<br>` if not dpop_proof:`<br>` raise ValueError("DPoP proof required")`<br>` return client.post("/payments/transfer", headers={"Authorization": f"Bearer {access_token}", "DPoP": dpop_proof})`<br>`# DPoP обязателен для высокорисковых операций ЦБ, чтобы снизить риск кражи токенов` | Identity/OIDC | `FAPI 2.0 Security Profile; OAuth 2.1 Draft; OWASP ASVS v4.0.3` | `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 обязателен для высокорисковых операций ЦБ, чтобы снизить риск кражи токенов` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: ak-010 dpop token theft fapi 2 0 высокорисковые операции цб proof required -->
13
+ | AK-011 | PII in JWT: конфиденциальные данные в открытом payload | `payload = {"sub": user_id, "email": email, "phone": phone, "name": full_name, "role": role}`<br>`token = jwt.encode(payload, private_key, algorithm="RS256")` | `payload = {"sub": user_id, "role": role, "scope": "api.read"}`<br>`token = jwt.encode(payload, private_key, algorithm="RS256")`<br>`# PII moved to userinfo endpoint or encrypted storage` | Identity/OIDC | `OAuth 2.1 Draft; OWASP ASVS v4.0.3` | `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` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: ak-011 pii jwt конфиденциальные данные в открытом payload sub user id email phone name full role token encode private key algorithm -->
14
+ | AK-012 | JWKS Rate Limiting: нет ограничений на запросы к `/.well-known/jwks.json` при неизвестном `kid` | `def get_jwk_for_kid(kid: str):`<br>` # every unknown kid triggers upstream call`<br>` return requests.get(f"{issuer}/.well-known/jwks.json", timeout=2).json()` | `def get_jwk_for_kid(kid: str):`<br>` if kid in negative_kid_cache and not negative_kid_cache[kid].expired:`<br>` raise ValueError("unknown kid cached")`<br>` if not jwks_rate_limiter.allow("jwks_fetch"):`<br>` raise RuntimeError("jwks rate limit exceeded")`<br>` jwks = requests.get(f"{issuer}/.well-known/jwks.json", timeout=2).json()`<br>` # cache keys and unknown kid misses`<br>` return select_key_from_jwks(jwks, kid)` | Identity/OIDC | `OAuth 2.1 Draft; OWASP ASVS v4.0.3` | `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)` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: ak-012 jwks rate limiting нет ограничений на запросы к well known json при неизвестном kid def get jwk for str every -->
15
+ | AK-013 | Insecure Token Forwarding: прямой проброс пользовательского JWT между микросервисами | `def call_internal_service(user_jwt: str):`<br>` return requests.get("http://orders.internal/api/orders", headers={"Authorization": f"Bearer {user_jwt}"})` | `def exchange_token(user_jwt: str, audience: str) -> str:`<br>` resp = requests.post(f"{issuer}/protocol/openid-connect/token", data={`<br>` "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",`<br>` "subject_token": user_jwt,`<br>` "subject_token_type": "urn:ietf:params:oauth:token-type:access_token",`<br>` "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",`<br>` "audience": audience,`<br>` }, auth=(client_id, client_secret), cert=(client_cert_path, client_key_path), timeout=5)`<br>` resp.raise_for_status()`<br>` return resp.json()["access_token"]`<br><br>`def call_internal_service(user_jwt: str):`<br>` svc_token = exchange_token(user_jwt, audience="orders-api")`<br>` return requests.get("http://orders.internal/api/orders", headers={"Authorization": f"Bearer {svc_token}"}, timeout=5)`<br>`# token exchange endpoint /token вызывать только по mTLS в профиле ФАПИ.ПАОК` | Identity/OIDC | `RFC 8693; FAPI 2.0 Security Profile` | `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 в профиле ФАПИ.ПАОК` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: ak-013 token forwarding mtls token exchange endpoint profile fapi paok keycloak -->
16
+ | AK-014 | Missing Resource Indicators: отсутствует параметр `resource` при запросе токена | `token_req = {`<br>` "grant_type": "authorization_code",`<br>` "code": code,`<br>` "redirect_uri": redirect_uri,`<br>`}`<br>`token = requests.post(token_url, data=token_req, auth=(client_id, client_secret), timeout=5)` | `token_req = {`<br>` "grant_type": "authorization_code",`<br>` "code": code,`<br>` "redirect_uri": redirect_uri,`<br>` "resource": "https://api.example.com/orders",`<br>`}`<br>`token = requests.post(token_url, data=token_req, auth=(client_id, client_secret), timeout=5)`<br>`token.raise_for_status()` | Identity/OIDC | `RFC 8707; FAPI 2.0 Security Profile` | `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()` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: ak-014 missing resource indicators отсутствует параметр при запросе токена token req grant type authorization redirect uri requests post url data auth -->
17
+ | AK-015 | OIDC State Validation Missing: callback не проверяет `state` | `@app.get("/oidc/callback")`<br>`async def callback(code: str, state: str):`<br>` ...`<br>` return await exchange_code(code)` | `@app.get("/oidc/callback")`<br>`async def callback(code: str, state: str, request: Request):`<br>` expected = request.session.get("oidc_state")`<br>` if not expected or state != expected:`<br>` raise HTTPException(status_code=401, detail="invalid state")`<br>` ...`<br>` return await exchange_code(code)` | Identity/OIDC | `CWE-384` | `@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)` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: ak-015 oidc state validation missing callback не проверяет app get async def str return await exchange request expected session if not -->
18
+ | AK-016 | OIDC Nonce Validation Missing: ID Token принимается без проверки `nonce` | `id_claims = jwt.decode(id_token, jwk, algorithms=["RS256"], audience=client_id, issuer=issuer)`<br>`...`<br>`return id_claims` | `id_claims = jwt.decode(id_token, jwk, algorithms=["RS256","ES256"], audience=client_id, issuer=issuer)`<br>`expected_nonce = request.session.get("oidc_nonce")`<br>`if not expected_nonce or id_claims.get("nonce") != expected_nonce:`<br>` raise HTTPException(status_code=401, detail="invalid nonce")`<br>`...`<br>`return id_claims` | Identity/OIDC | `CWE-346` | `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` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: ak-016 oidc nonce validation missing id token принимается без проверки claims jwt decode jwk algorithms rs256 audience client issuer return es256 -->
19
+ | AK-017 | Session Management: нет принудительного logout и refresh_token TTL > 24ч | `refresh_token_ttl = 864000`<br>`# no back-channel/front-channel logout` | `refresh_token_ttl = 86400`<br>`if refresh_token_ttl > 86400:`<br>` raise ValueError("CB session limit exceeded")`<br>`enable_backchannel_logout = True`<br>`enable_frontchannel_logout = True`<br>`revoke_refresh_token_on_logout = True` | Identity/OIDC | `ГОСТ 57580.1 (IA); требования ЦБ по управлению сессиями` | `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` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: ak-017 session management logout refresh token ttl 24 hours cb gost 57580 keycloak -->
20
+ | AK-018 | Zero Trust mTLS: межсервисные вызовы выполняются без mTLS | `requests.get("http://orders.internal/api")`<br>`grpc.insecure_channel("billing.internal:50051")` | Все межсервисные вызовы выполнять по mTLS (service identity, cert pinning, trust policy), не только token exchange endpoint. | Identity/OIDC | NIST SP 800-207 (Zero Trust) | Все межсервисные вызовы выполнять по mTLS (service identity, cert pinning, trust policy), не только token exchange endpoint. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: ak-018 zero trust mtls service to service nist 800-207 keycloak mesh -->
21
+ | AK-019 | ASVS L3 Admin Session: отсутствует ротация секретов и ограничение админ-сессий | `admin_session_ttl = 7*24*3600`<br>`admin_client_secret = "static-secret"` | Для админ-учетных записей принудительная ротация клиентских секретов, короткий TTL сессий, step-up auth и немедленный revoke при logout/risk events. | Identity/OIDC | OWASP ASVS v4.0 (L3), NIST 800-63 | Для админ-учетных записей принудительная ротация клиентских секретов, короткий TTL сессий, step-up auth и немедленный revoke при logout/risk events. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: ak-019 asvs l3 admin session ttl secret rotation keycloak revoke step-up auth -->
22
+ | AK-020 | jose.jwt.decode with `verify_signature=False` | `jwt.decode(token, key, options={"verify_signature": False})` | Никогда не отключать проверку подписи; валидировать подпись JWT по JWKS и reject токены с invalid signature. | Identity/OIDC | CWE Final Certification | Никогда не отключать проверку подписи; валидировать подпись JWT по JWKS и reject токены с invalid signature. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: ak-020 jose jwt decode verify_signature false -->
23
+ | AK-021 | jose.jwt.decode without explicit algorithms allowlist | `jwt.decode(token, key, audience=aud, issuer=iss)` | Всегда указывать `algorithms=["RS256"]` (или строгий allowlist) и запрещать algorithm confusion/fallback. | Identity/OIDC | CWE Final Certification | Всегда указывать `algorithms=["RS256"]` (или строгий allowlist) и запрещать algorithm confusion/fallback. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: ak-021 jose jwt decode without algorithms allowlist rs256 -->
24
+ | AK-022 | Session fixation: session id not rotated after successful login | `session.setAttribute("uid", uid)` | regenerate session id on auth success and invalidate old session | Identity/OIDC | CWE-384 | Rotate session identifiers after authentication. |
25
+ | AK-023 | Session fixation: auth cookie preserves pre-auth identifier | `sid = request.cookies.get("AUTH_SESSION_ID")`<br>`response.set_cookie("AUTH_SESSION_ID", sid)` | `response.delete_cookie("PRE_AUTH_ID")`<br>`response.set_cookie("AUTH_SESSION_ID", new_session_id(), httponly=True, secure=True)` | Identity/OIDC | CWE-384 | Split pre-auth and post-auth cookie scopes. |
26
+ | AK-024 | Token renewal logic: refresh token accepted after logout | `def logout(rt): pass # no revoke` | `def logout(rt): revoke_refresh_family(rt)` | Identity/OIDC | CWE-613 | Enforce immediate refresh revocation. |
27
+ | AK-025 | Token renewal logic: no reuse-detection for rotated refresh tokens | `if validate(old_rt) or validate(new_rt): issue_token()` | `if is_reuse_detected(rt): revoke_session()` | Identity/OIDC | CWE-294 | Bind refresh lifecycle to one-time use. |
28
+ | AK-026 | Session fixation: remember-me token bound only to username | `token = sign({"u": username})` | `token = sign({"u": username, "dev": device_id, "sid": sid})` | Identity/OIDC | CWE-384 | Add contextual token binding. |
29
+ | AK-027 | Token renewal logic: sliding session extends indefinitely | `session.expires_at = now + idle_ttl # forever` | `session.expires_at = min(now + idle_ttl, abs_deadline)` | Identity/OIDC | CWE-613 | Cap absolute SSO session TTL. |
30
+ | AK-028 | Session fixation: login endpoint accepts externally supplied session id | `sid = request.cookies.get("JSESSIONID")`<br>`login_with_session(sid)` | `sid = new_session_id()`<br>`response.set_cookie("JSESSIONID", sid, httponly=True, secure=True)` | Identity/OIDC | CWE-384 | Server controls all session identifiers. |
31
+ | AK-029 | Token renewal logic: missing audience check on refreshed access token | `token = refresh(rt) # no aud check` | `token = refresh(rt, audience=resource_aud)` | Identity/OIDC | CWE-345 | Scope refreshed tokens per resource. |
32
+ | AK-030 | Session fixation: parallel login keeps stale sessions active | `create_new_session(user)` | `invalidate_user_sessions(user); create_new_session(user)` | Identity/OIDC | CWE-613 | Enforce single-session policy for privileged users. |
33
+ | AK-031 | Token renewal logic: refresh endpoint lacks client binding | `refresh(rt, client_id=req.client_id)` | `assert rt.client_id == req.client_id; refresh(rt)` | Identity/OIDC | CWE-290 | Bind refresh tokens to originating client. |
34
+ | AK-032 | Session fixation: SSO cookie lacks SameSite/HttpOnly/Secure hardening | `set_cookie("SSO", sid)` | `set_cookie("SSO", sid, secure=True, httponly=True, samesite="Strict")` | Identity/OIDC | CWE-614 | Harden session cookies in all realms. |
35
+ | AK-033 | Token renewal logic: no nonce/jti replay check on renewed tokens | `if validate(rt): issue_access()` | `if replay_cache.seen(jti): deny(); replay_cache.add(jti)` | Identity/OIDC | CWE-294 | Reject replayed token identifiers. |
36
+ | AK-034 | Session fixation: authentication flow shares session across tabs without state isolation | `session["state"]=state` | `session[f"state:{tab_id}"]=state` | Identity/OIDC | CWE-384 | Isolate browser tab auth context. |
37
+ | AK-035 | Token renewal logic: refresh token not invalidated after password change | `change_password(user, pwd)` | `change_password(user,pwd); revoke_all_refresh(user)` | Identity/OIDC | CWE-613 | Force re-authentication after password updates. |
38
+ | AK-036 | Session fixation: brokered identity login keeps pre-broker session id | `broker_callback(); login_success()` | `rotate_session_id(); broker_callback(); login_success()` | Identity/OIDC | CWE-384 | Regenerate server session in broker flow. |
39
+ | AK-037 | Token renewal logic: long-lived offline token issued without policy gates | `issue_offline_token(client)` | `if client in approved_confidential: issue_offline_token(client)` | Identity/OIDC | CWE-250 | Policy-gate offline token issuance. |
40
+ | AK-038 | Session fixation: failed login increments same session context exposing enumeration side-channel | `return "user_not_found"` | `return "invalid_credentials"` | Identity/OIDC | CWE-203 | Normalize error and context behavior. |
41
+ | AK-039 | Token renewal logic: refresh endpoint lacks mTLS/DPoP for high-risk clients | `refresh(rt)` | `require_sender_constrained(rt, mtls_or_dpop_proof); refresh(rt)` | Identity/OIDC | CWE-287 | Add proof-of-possession for renewal. |
42
+ | AK-040 | Session fixation: admin console session not re-authenticated for sensitive actions | `perform_admin_action()` | `step_up_reauth(); perform_admin_action()` | Identity/OIDC | CWE-306 | Re-authenticate before high-risk operations. |
43
+ | AK-041 | Token renewal logic: no clock-skew guard for `iat/nbf` on refreshed tokens | `if validate_sig(token): accept()` | `validate_time_claims(token, max_skew=30)` | Identity/OIDC | CWE-345 | Enforce temporal validation window. |
44
+ | AK-042 | Session fixation: auth session not rotated on OAuth code callback | `handle_callback(); establish_session(old_sid)` | `rotate_session_id(); establish_session(new_sid)` | Identity/OIDC | CWE-384 | Rotate SID after external auth callback. |
45
+ | AK-043 | Token renewal logic: refresh chain not invalidated after suspicious IP/device change | `ctx = fingerprint(request)`<br>`refresh(rt, ctx)` | `ctx = fingerprint(request)`<br>`if context_changed(rt, ctx): revoke_chain(rt); require_reauth()` | Identity/OIDC | CWE-613 | Revoke refresh family on risk events. |
46
+ | AK-044 | Token renewal logic: refresh endpoint accepts token with missing `jti` | `if validate_sig(rt): issue_new()` | `require_jti(rt); reject_if_missing_or_replayed(rt.jti)` | Identity/OIDC | CWE-345 | Enforce replay-safe refresh identifiers. |
@@ -0,0 +1,51 @@
1
+ {
2
+ "skill_id": "auth-keycloak",
3
+ "name": "Auth / Keycloak / OIDC",
4
+ "activation_triggers": [
5
+ "ak-keycloak-oidc",
6
+ "ak-jwks-kid",
7
+ "ak-pkce-flow",
8
+ "ak-token-exchange-rfc8693",
9
+ "ak-dpop-proof",
10
+ "fapi-sec",
11
+ "fapi-paok",
12
+ "clinker-auth",
13
+ "gost-crypto-jwt"
14
+ ],
15
+ "relevant_extensions": [
16
+ ".py",
17
+ ".ts",
18
+ ".js",
19
+ ".java"
20
+ ],
21
+ "tools": [
22
+ "semgrep",
23
+ "syft",
24
+ "trufflehog"
25
+ ],
26
+ "rules_path": "core/skills/auth-keycloak/patterns.md",
27
+ "few_shot_examples": "core/gold-standard-testbed/api_vulnerable.py",
28
+ "mitigation_logic": {
29
+ "AK-002": {
30
+ "exception_rule": "Если KEYCLOAK_VERIFY_AUD=false, но аудит Keycloak Realm подтверждает наличие только одного активного client_id=dion-agent, нарушение классифицируется как изолированный контур.",
31
+ "status_override": "OK",
32
+ "priority_override": "Low",
33
+ "label": "Isolated Realm",
34
+ "evidence_requirements": [
35
+ "Экспорт/аудит конфигурации realm с единственным активным клиентом.",
36
+ "Отсутствие доверенных внешних audience для токенов данного realm."
37
+ ]
38
+ },
39
+ "AK-020": {
40
+ "exception_rule": "Предварительный decode JWT без verify_signature допускается только как технический pre-parse этап, если в той же цепочке вызовов гарантирована последующая обязательная верификация подписи через JWKS.",
41
+ "status_override": "OK",
42
+ "validation_requirements": [
43
+ "Результат pre-decode не используется для authorization decision до успешной подписи/claims валидации.",
44
+ "После pre-decode выполняется jwt.decode(..., options={\"verify_signature\": true}) или эквивалентная проверка по актуальному JWKS.",
45
+ "При неуспехе JWKS/подписи обработка прерывается fail-closed."
46
+ ],
47
+ "rationale": "Двухэтапная схема parse->verify безопасна при строгом запрете доверия данным токена до криптографической верификации."
48
+ }
49
+ },
50
+ "security_priority": 10
51
+ }
@@ -0,0 +1,58 @@
1
+ # Browser Agent (Playwright / automation)
2
+
3
+ ## Stack overview
4
+
5
+ **Playwright**-driven automation in Python and JavaScript: sandbox, navigation, downloads, and script execution boundaries. Metrics are prefixed **`BRW`**.
6
+
7
+ ## Top threats
8
+
9
+ - Unsafe Chromium flags and TLS downgrades (`BRW-001`–`BRW-003`, `BRW-006`).
10
+ - SSRF and local metadata access via `goto` (`BRW-007`, `BRW-008`).
11
+ - XSS / JS injection / prototype pollution in bridged code (`BRW-011`–`BRW-013`).
12
+
13
+ ## Pattern catalog
14
+
15
+ Complete Anti-Pattern / Safe-Pattern definitions live in [`patterns.md`](patterns.md). The table below is a **table of contents** by metric ID.
16
+
17
+ | ID | Metric | Stack |
18
+ |---|---|---|
19
+ | `BRW-001` | Playwright: запуск без sandbox (`--no-sandbox`) | `browser = await p.chromium.launch(` ` args=[],` ` headless=True,` `)` |
20
+ | `BRW-002` | Playwright: `ignoreHTTPSErrors=True` | `context = await browser.new_context(ignoreHTTPSErrors=False)` `page = await context.new_page()` |
21
+ | `BRW-003` | Prod: `headless: false` | `browser = await p.chromium.launch(headless=True)` |
22
+ | `BRW-004` | WebRTC metadata leakage через page.evaluate() | `await page.route(\"**/*\", lambda route: route.abort() if \"stun\" in route.request.url else route.continue_())` |
23
+ | `BRW-005` | Пользовательский JS через page.evaluate() | `allowed = {\"scrollToTop\",\"extractText\"}` `cmd = request.json()[\"cmd\"]` `if cmd not in allowed: raise ValueError(\"cmd rejected\")` `await page.evaluate(\"(arg) => window.scrollTo(0,0)\", None)` |
24
+ | `BRW-006` | Отключение защитных флагов Chromium | `browser = await p.chromium.launch(args=[])` |
25
+ | `BRW-007` | File Protocol Restriction: `file://` разрешен в `page.goto()` | `target = user_input_url` `if target.startswith(\"file://\"):` ` raise ValueError(\"file protocol is forbidden\")` `await page.goto(target, wait_until=\"domcontentloaded\")` |
26
+ | `BRW-008` | SSRF via Browser: доступ к localhost/metadata endpoints | `import ipaddress` `from urllib.parse import urlparse` `def _blocked_host(host: str) -> bool:` ` if host in {\"localhost\"}:` ` return True` ` try:` ` ip = ipaddress.ip_address(host)` ` return ip.is_loopback or ip.is_private or ip.is_link_local` ` except ValueError:` ` return False` `parsed = urlparse(url)` `if _blocked_host(parsed.hostname or \"\") or (parsed.hostname == \"169.254.169.254\"):` ` raise ValueError(\"blocked destination\")` `await page.goto(url, wait_until=\"domcontentloaded\")` |
27
+ | `BRW-009` | Zombies & Leaks: контекст не закрывается, timeout не задан | `context = await browser.new_context()` `try:` ` page = await context.new_page()` ` page.set_default_navigation_timeout(10000)` ` await page.goto(url, timeout=10000, wait_until=\"domcontentloaded\")` `finally:` ` await context.close()` |
28
+ | `BRW-010` | Download Restrictions: автоскачивание включено, MIME не проверяется | `context = await browser.new_context(accept_downloads=False)` `page = await context.new_page()` `resp = await page.goto(url, wait_until=\"domcontentloaded\")` `content_type = (resp.headers.get(\"content-type\", \"\") if resp else \"\")` `allowed = {\"text/html\", \"application/json\"}` `if content_type.split(\";\")[0] not in allowed:` ` raise ValueError(\"blocked MIME type\")` |
29
+ | `BRW-011` | DOM XSS: пользовательский контент вставляется через `innerHTML` | `const note = request.body.note` `...` `await page.evaluate((value) => { document.querySelector("#out").textContent = value }, note)` |
30
+ | `BRW-012` | JS Injection: выполнение пользовательского JS через `eval`/`new Function` | `const cmd = request.body.cmd` `allowed = {"scrollTop":"window.scrollTo(0,0)"}` `if (!(cmd in allowed)) throw new Error("cmd rejected")` `...` `await page.evaluate(allowed[cmd])` |
31
+ | `BRW-013` | Prototype Pollution: запись в `__proto__` / merge без фильтра ключей | `const patch = request.body.patch` `for (const k of Object.keys(patch)) {` ` if (["__proto__","constructor","prototype"].includes(k)) throw new Error("blocked key")` `}` `...` `Object.assign(config, patch)` |
32
+
33
+ ## Verification
34
+
35
+ **Verification:** Check the gold testbed file(s) below for `Vulnerable: <ID>` markers (static Semgrep + `detection-matrix.md` ground truth).
36
+
37
+ - [`gold-standard-testbed/browser_vulnerable.js`](../gold-standard-testbed/browser_vulnerable.js)
38
+
39
+ After changing [`patterns.md`](patterns.md), run from the repo root:
40
+
41
+ ```bash
42
+ python scripts/sync_semgrep.py
43
+ ```
44
+
45
+ ## Workflow: Recon → Scan → Verify
46
+
47
+ ### 1) Recon
48
+ - Map entrypoints, data flows, and trust boundaries for this stack.
49
+ - Identify which metrics in [`patterns.md`](patterns.md) apply to the code under review.
50
+
51
+ ### 2) Scan
52
+ - Run Semgrep with `semgrep-rules/<skill>.yaml` (generated) and correlate with Anti-Patterns.
53
+ - Eliminate findings that cannot bind to a metric row.
54
+
55
+ ### 3) Verify
56
+ - Confirm markers or scanner hits for touched IDs in the gold testbed when adding metrics.
57
+ - Emit findings as `Vulnerable: <PREFIX>-<NNN>` in written reviews.
58
+
@@ -0,0 +1,15 @@
1
+ | ID | Название метрики | Anti-Pattern (Vulnerable Code/YAML) | Safe-Pattern (Remediation) | Stack | Источник fix_template | Exploit scenario |
2
+ |---|---|---|---|---|---|---|
3
+ | BRW-001 | Playwright: запуск без sandbox (`--no-sandbox`) | `browser = await p.chromium.launch(`<br>` args=[\"--no-sandbox\",\"--disable-setuid-sandbox\"],`<br>` headless=True,`<br>`)` | `browser = await p.chromium.launch(`<br>` args=[],`<br>` headless=True,`<br>`)` | Browser Automation | `Ошибка доступа к @Docs (Playwright/Selenium)` | `browser = await p.chromium.launch(` ` args=[],` ` headless=True,` `)` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: brw-001 playwright запуск без sandbox no browser await p chromium launch args disable setuid headless true -->
4
+ | BRW-002 | Playwright: `ignoreHTTPSErrors=True` | `context = await browser.new_context(ignoreHTTPSErrors=True)`<br>`page = await context.new_page()` | `context = await browser.new_context(ignoreHTTPSErrors=False)`<br>`page = await context.new_page()` | Browser Automation | `Ошибка доступа к @Docs (Playwright/Selenium)` | `context = await browser.new_context(ignoreHTTPSErrors=False)` `page = await context.new_page()` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: brw-002 playwright ignorehttpserrors true context await browser new page false -->
5
+ | BRW-003 | Prod: `headless: false` | `browser = await p.chromium.launch(headless=False)` | `browser = await p.chromium.launch(headless=True)` | Browser Automation | `Ошибка доступа к @Docs (Playwright/Selenium)` | `browser = await p.chromium.launch(headless=True)` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: brw-003 prod headless false browser await p chromium launch true -->
6
+ | BRW-004 | WebRTC metadata leakage через page.evaluate() | `await page.evaluate(() => new RTCPeerConnection().createOffer())` | `await page.route(\"**/*\", lambda route: route.abort() if \"stun\" in route.request.url else route.continue_())` | Browser Automation | `Ошибка доступа к @Docs (Playwright/Selenium)` | `await page.route(\"**/*\", lambda route: route.abort() if \"stun\" in route.request.url else route.continue_())` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: brw-004 webrtc metadata leakage через page evaluate await new rtcpeerconnection createoffer route lambda abort if stun request url else continue -->
7
+ | BRW-005 | Пользовательский JS через page.evaluate() | `user_js = request.json()[\"script\"]`<br>`await page.evaluate(user_js)` | `allowed = {\"scrollToTop\",\"extractText\"}`<br>`cmd = request.json()[\"cmd\"]`<br>`if cmd not in allowed: raise ValueError(\"cmd rejected\")`<br>`await page.evaluate(\"(arg) => window.scrollTo(0,0)\", None)` | Browser Automation | `Ошибка доступа к @Docs (Playwright/Selenium)` | `allowed = {\"scrollToTop\",\"extractText\"}` `cmd = request.json()[\"cmd\"]` `if cmd not in allowed: raise ValueError(\"cmd rejected\")` `await page.evaluate(\"(arg) => window.scrollTo(0,0)\", None)` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: brw-005 пользовательский js через page evaluate user request json script await allowed scrolltotop extracttext cmd if not raise valueerror rejected arg -->
8
+ | BRW-006 | Отключение защитных флагов Chromium | `browser = await p.chromium.launch(`<br>` args=[\"--disable-web-security\",\"--disable-site-isolation-trials\"],`<br>`)` | `browser = await p.chromium.launch(args=[])` | Browser Automation | `Ошибка доступа к @Docs (Playwright/Selenium)` | `browser = await p.chromium.launch(args=[])` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: brw-006 отключение защитных флагов chromium browser await p launch args disable web security site isolation trials -->
9
+ | BRW-007 | File Protocol Restriction: `file://` разрешен в `page.goto()` | `target = user_input_url`<br>`await page.goto(target)`<br>`# attacker can pass file:///etc/passwd` | `target = user_input_url`<br>`if target.startswith(\"file://\"):`<br>` raise ValueError(\"file protocol is forbidden\")`<br>`await page.goto(target, wait_until=\"domcontentloaded\")` | Browser Automation | `Playwright Security Best Practices; OWASP Web Security Testing Guide` | `target = user_input_url` `if target.startswith(\"file://\"):` ` raise ValueError(\"file protocol is forbidden\")` `await page.goto(target, wait_until=\"domcontentloaded\")` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: brw-007 file protocol restriction разрешен в page goto target user input url await attacker can pass etc passwd if startswith raise -->
10
+ | BRW-008 | SSRF via Browser: доступ к localhost/metadata endpoints | `await page.goto(url)`<br>`# url may be http://127.0.0.1:... or http://169.254.169.254/...` | `import ipaddress`<br>`from urllib.parse import urlparse`<br><br>`def _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_loopback or ip.is_private or ip.is_link_local`<br>` except ValueError:`<br>` return False`<br><br>`parsed = urlparse(url)`<br>`if _blocked_host(parsed.hostname or \"\") or (parsed.hostname == \"169.254.169.254\"):`<br>` raise ValueError(\"blocked destination\")`<br>`await page.goto(url, wait_until=\"domcontentloaded\")` | Browser Automation | `Playwright Security Best Practices; OWASP Web Security Testing Guide` | `import ipaddress` `from urllib.parse import urlparse` `def _blocked_host(host: str) -> bool:` ` if host in {\"localhost\"}:` ` return True` ` try:` ` ip = ipaddress.ip_address(host)` ` return ip.is_loopback or ip.is_private or ip.is_link_local` ` except ValueError:` ` return False` `parsed = urlparse(url)` `if _blocked_host(parsed.hostname or \"\") or (parsed.hostname == \"169.254.169.254\"):` ` raise ValueError(\"blocked destination\")` `await page.goto(url, wait_until=\"domcontentloaded\")` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: brw-008 ssrf via browser доступ к localhost metadata endpoints await page goto url may be http 127 0 1 169 254 -->
11
+ | BRW-009 | Zombies & Leaks: контекст не закрывается, timeout не задан | `context = await browser.new_context()`<br>`page = await context.new_page()`<br>`await page.goto(url)`<br>`# context.close() missing` | `context = await browser.new_context()`<br>`try:`<br>` page = await context.new_page()`<br>` page.set_default_navigation_timeout(10000)`<br>` await page.goto(url, timeout=10000, wait_until=\"domcontentloaded\")`<br>`finally:`<br>` await context.close()` | Browser Automation | `Playwright Security Best Practices; OWASP Web Security Testing Guide` | `context = await browser.new_context()` `try:` ` page = await context.new_page()` ` page.set_default_navigation_timeout(10000)` ` await page.goto(url, timeout=10000, wait_until=\"domcontentloaded\")` `finally:` ` await context.close()` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: brw-009 zombies leaks контекст не закрывается timeout задан context await browser new page goto url close missing try set default navigation -->
12
+ | BRW-010 | Download Restrictions: автоскачивание включено, MIME не проверяется | `context = await browser.new_context(accept_downloads=True)`<br>`download = await page.wait_for_event(\"download\")`<br>`path = await download.path()` | `context = await browser.new_context(accept_downloads=False)`<br>`page = await context.new_page()`<br>`resp = await page.goto(url, wait_until=\"domcontentloaded\")`<br>`content_type = (resp.headers.get(\"content-type\", \"\") if resp else \"\")`<br>`allowed = {\"text/html\", \"application/json\"}`<br>`if content_type.split(\";\")[0] not in allowed:`<br>` raise ValueError(\"blocked MIME type\")` | Browser Automation | `Playwright Security Best Practices; OWASP Web Security Testing Guide` | `context = await browser.new_context(accept_downloads=False)` `page = await context.new_page()` `resp = await page.goto(url, wait_until=\"domcontentloaded\")` `content_type = (resp.headers.get(\"content-type\", \"\") if resp else \"\")` `allowed = {\"text/html\", \"application/json\"}` `if content_type.split(\";\")[0] not in allowed:` ` raise ValueError(\"blocked MIME type\")` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: brw-010 download restrictions автоскачивание включено mime не проверяется context await browser new accept downloads true page wait for event path false -->
13
+ | BRW-011 | DOM XSS: пользовательский контент вставляется через `innerHTML` | `const note = request.body.note`<br>`...`<br>`await page.evaluate((value) => { document.querySelector("#out").innerHTML = value }, note)` | `const note = request.body.note`<br>`...`<br>`await page.evaluate((value) => { document.querySelector("#out").textContent = value }, note)` | Browser Automation | `CWE-79` | `const note = request.body.note` `...` `await page.evaluate((value) => { document.querySelector("#out").textContent = value }, note)` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: brw-011 dom xss пользовательский контент вставляется через innerhtml const note request body await page evaluate value document queryselector out textcontent -->
14
+ | BRW-012 | JS Injection: выполнение пользовательского JS через `eval`/`new Function` | `const script = request.body.script`<br>`...`<br>`await page.evaluate((s) => eval(s), script)`<br>`...`<br>`const fn = new Function(script)` | `const cmd = request.body.cmd`<br>`allowed = {"scrollTop":"window.scrollTo(0,0)"}`<br>`if (!(cmd in allowed)) throw new Error("cmd rejected")`<br>`...`<br>`await page.evaluate(allowed[cmd])` | Browser Automation | `CWE-95` | `const cmd = request.body.cmd` `allowed = {"scrollTop":"window.scrollTo(0,0)"}` `if (!(cmd in allowed)) throw new Error("cmd rejected")` `...` `await page.evaluate(allowed[cmd])` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: brw-012 js injection выполнение пользовательского через eval new function const script request body await page evaluate s fn cmd allowed scrolltop -->
15
+ | BRW-013 | Prototype Pollution: запись в `__proto__` / merge без фильтра ключей | `const patch = request.body.patch`<br>`...`<br>`target.__proto__ = patch`<br>`...`<br>`Object.assign(config, patch)` | `const patch = request.body.patch`<br>`for (const k of Object.keys(patch)) {`<br>` if (["__proto__","constructor","prototype"].includes(k)) throw new Error("blocked key")`<br>`}`<br>`...`<br>`Object.assign(config, patch)` | Browser Automation | `CWE-1321` | `const patch = request.body.patch` `for (const k of Object.keys(patch)) {` ` if (["__proto__","constructor","prototype"].includes(k)) throw new Error("blocked key")` `}` `...` `Object.assign(config, patch)` | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: brw-013 prototype pollution запись в proto merge без фильтра ключей const patch request body target object assign config for k of -->
@@ -0,0 +1,24 @@
1
+ {
2
+ "skill_id": "browser-agent",
3
+ "name": "Browser Agent Security",
4
+ "activation_triggers": [
5
+ "brw-playwright-sandbox",
6
+ "brw-selenium-driver",
7
+ "brw-headless-flags",
8
+ "brw-browser-ssrf",
9
+ "brw-dom-injection"
10
+ ],
11
+ "relevant_extensions": [
12
+ ".py",
13
+ ".js",
14
+ ".ts"
15
+ ],
16
+ "tools": [
17
+ "semgrep",
18
+ "syft",
19
+ "trufflehog"
20
+ ],
21
+ "rules_path": "core/skills/browser-agent/patterns.md",
22
+ "few_shot_examples": "core/gold-standard-testbed/browser_vulnerable.js",
23
+ "security_priority": 5
24
+ }
@@ -0,0 +1,66 @@
1
+ # Cloud & Secrets
2
+
3
+ ## Stack overview
4
+
5
+ Cloud-native secret management and workload hardening across **Kubernetes YAML** and **Python services**: metadata SSRF, IAM/KMS/Vault hygiene, ENV/log leakage, and JWT trust boundaries. Metrics are prefixed **`SEC`**.
6
+
7
+ **Product alignment:** cloud workload hardening, metadata egress controls, and secret governance must map to SEC metrics before release.
8
+
9
+ ## Top threats
10
+
11
+ - Metadata SSRF and exposed cloud identity surfaces (`SEC-001`, `SEC-007`).
12
+ - Kubernetes workload misconfigurations around secrets and privilege (`SEC-002`–`SEC-005`).
13
+ - Secret leakage to logs, endpoints, and CI output (`SEC-006`, `SEC-013`, `SEC-015`).
14
+ - JWT/Vault/KMS misuse and missing secret lifecycle controls (`SEC-008`–`SEC-012`, `SEC-014`).
15
+
16
+ ## Pattern catalog
17
+
18
+ Complete Anti-Pattern / Safe-Pattern definitions live in [`patterns.md`](patterns.md). The table below is a **table of contents** by metric ID.
19
+
20
+ | ID | Metric | Stack |
21
+ |---|---|---|
22
+ | `SEC-001` | SSRF to Cloud Metadata API | Блокировать link-local metadata endpoints в egress policy; использовать IMDSv2/metadata proxy с явной авторизацией. |
23
+ | `SEC-002` | K8s Secret in ConfigMap | Хранить секреты только в `Secret`/external secrets manager (Vault/KMS), шифровать at-rest и ограничить RBAC. |
24
+ | `SEC-003` | Privileged Container | `privileged: false`, `allowPrivilegeEscalation: false`, `runAsNonRoot: true`, минимальные capabilities. |
25
+ | `SEC-004` | HostPath Mount of Sensitive Paths | Запретить опасные `hostPath` mounts; использовать CSI/ephemeral volumes с ограниченными правами. |
26
+ | `SEC-005` | Service Account Token Auto-Mount | Отключить auto-mount по умолчанию; выдавать токен только workload-ам, которым он необходим. |
27
+ | `SEC-006` | ENV Secret Leakage to Logs | Никогда не логировать весь ENV; применять allowlist полей и redaction для секретов. |
28
+ | `SEC-007` | Hardcoded Cloud Credentials | Использовать workload identity / IAM role / short-lived STS tokens без hardcode. |
29
+ | `SEC-008` | Insecure JWT Validation | Проверять подпись, `issuer`, `audience`, `exp`, `nbf`, `alg` allowlist. |
30
+ | `SEC-009` | Vault Token in Plain Config | Хранить Vault auth через AppRole/K8s auth + short-lived token, ротацию и scoped policies; обязательно использовать Vault Agent Injector для автоматической доставки и ротации токенов/секретов в workload. |
31
+ | `SEC-010` | Vault TLS Verification Disabled | Всегда `verify=True`, mTLS/CA pinning, запрет insecure transport. |
32
+ | `SEC-011` | Unencrypted Secret in Object Storage | Включить SSE-KMS/CMK, ограничить доступ bucket policy и включить audit trail. |
33
+ | `SEC-012` | Broad KMS Permissions | Принцип least privilege: ограничить actions/resources и контекст ключей. |
34
+ | `SEC-013` | Publicly Exposed Secrets Endpoint | Удалить/закрыть debug endpoints, включить authz + environment gating для non-prod only. |
35
+ | `SEC-014` | Missing Secret Rotation Policy | Обязательная ротация секретов/ключей (TTL), автоматизация revoke/renew и контроль просрочки. |
36
+ | `SEC-015` | Unsafe Secret in CI Variables | Masked/protected CI variables, secret scanning в pipeline, запрет echo/print секретов. |
37
+ | `SEC-016` | External Secrets Operator Required | Использовать `kind: ExternalSecret`, ссылающийся на `SecretStore/ClusterSecretStore` (Vault backend), и исключить хранение секретов в Git. |
38
+ | `SEC-017` | Trusted Mounts for DB Passwords | Передавать секреты как файлы через `volumeMounts` (Vault Agent Injector или ESO synced volume), читать пароль из файловой системы, а не из ENV. |
39
+
40
+ ## Verification
41
+
42
+ **Verification:** Check the gold testbed file(s) below for `Vulnerable: <ID>` markers (static Semgrep + `detection-matrix.md` ground truth).
43
+
44
+ - [`gold-standard-testbed/cloud_secrets_vulnerable.py`](../gold-standard-testbed/cloud_secrets_vulnerable.py)
45
+ - [`gold-standard-testbed/cloud_secrets_vulnerable.yaml`](../gold-standard-testbed/cloud_secrets_vulnerable.yaml)
46
+
47
+ After changing [`patterns.md`](patterns.md), run from the repo root:
48
+
49
+ ```bash
50
+ python scripts/sync_semgrep.py
51
+ ```
52
+
53
+ ## Workflow: Recon → Scan → Verify
54
+
55
+ ### 1) Recon
56
+ - Map entrypoints, data flows, and trust boundaries for this stack.
57
+ - Identify which metrics in [`patterns.md`](patterns.md) apply to the code under review.
58
+
59
+ ### 2) Scan
60
+ - Run Semgrep with `semgrep-rules/<skill>.yaml` (generated) and correlate with Anti-Patterns.
61
+ - Eliminate findings that cannot bind to a metric row.
62
+
63
+ ### 3) Verify
64
+ - Confirm markers or scanner hits for touched IDs in the gold testbed when adding metrics.
65
+ - Emit findings as `Vulnerable: <PREFIX>-<NNN>` in written reviews.
66
+
@@ -0,0 +1,19 @@
1
+ | ID | Название метрики | Anti-Pattern (Vulnerable Code/YAML) | Safe-Pattern (Remediation) | Stack | Источник fix_template | Exploit scenario |
2
+ |---|---|---|---|---|---|---|
3
+ | SEC-001 | SSRF to Cloud Metadata API | `requests.get("http://169.254.169.254/latest/meta-data/")` | Блокировать link-local metadata endpoints в egress policy; использовать IMDSv2/metadata proxy с явной авторизацией. | Cloud/Secrets | `CWE-918` | Блокировать link-local metadata endpoints в egress policy; использовать IMDSv2/metadata proxy с явной авторизацией. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: sec-001 ssrf to cloud metadata api requests get http 169 254 latest meta data блокировать link local endpoints в egress policy -->
4
+ | SEC-002 | K8s Secret in ConfigMap | `kind: ConfigMap`<br>`data:`<br>` DB_PASSWORD: supersecret` | Хранить секреты только в `Secret`/external secrets manager (Vault/KMS), шифровать at-rest и ограничить RBAC. | Cloud/Secrets | `CWE-798` | Хранить секреты только в `Secret`/external secrets manager (Vault/KMS), шифровать at-rest и ограничить RBAC. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: sec-002 k8s secret configmap kind data db password supersecret хранить секреты только в external secrets manager vault kms шифровать at rest -->
5
+ | SEC-003 | Privileged Container | `securityContext:`<br>` privileged: true` | `privileged: false`, `allowPrivilegeEscalation: false`, `runAsNonRoot: true`, минимальные capabilities. | Cloud/Secrets | `CWE-250` | `privileged: false`, `allowPrivilegeEscalation: false`, `runAsNonRoot: true`, минимальные capabilities. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: sec-003 privileged container securitycontext true false allowprivilegeescalation runasnonroot минимальные capabilities -->
6
+ | SEC-004 | HostPath Mount of Sensitive Paths | `hostPath:`<br>` path: /var/run/docker.sock` | Запретить опасные `hostPath` mounts; использовать CSI/ephemeral volumes с ограниченными правами. | Cloud/Secrets | `CWE-284` | Запретить опасные `hostPath` mounts; использовать CSI/ephemeral volumes с ограниченными правами. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: sec-004 hostpath mount of sensitive paths path var run docker sock запретить опасные mounts использовать csi ephemeral volumes с ограниченными правами -->
7
+ | SEC-005 | Service Account Token Auto-Mount | `automountServiceAccountToken: true` | Отключить auto-mount по умолчанию; выдавать токен только workload-ам, которым он необходим. | Cloud/Secrets | `CWE-200` | Отключить auto-mount по умолчанию; выдавать токен только workload-ам, которым он необходим. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: sec-005 service account token auto mount automountserviceaccounttoken true отключить по умолчанию выдавать токен только workload ам которым он необходим -->
8
+ | SEC-006 | ENV Secret Leakage to Logs | `logger.info("env=%s", os.environ)` | Никогда не логировать весь ENV; применять allowlist полей и redaction для секретов. | Cloud/Secrets | `CWE-532` | Никогда не логировать весь ENV; применять allowlist полей и redaction для секретов. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: sec-006 env secret leakage to logs logger info s os environ никогда не логировать весь применять allowlist полей и redaction для -->
9
+ | SEC-007 | Hardcoded Cloud Credentials | `AWS_SECRET_ACCESS_KEY = "AKIA..."` | Использовать workload identity / IAM role / short-lived STS tokens без hardcode. | Cloud/Secrets | `CWE-798` | Использовать workload identity / IAM role / short-lived STS tokens без hardcode. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: sec-007 hardcoded cloud credentials aws secret access key akia использовать workload identity iam role short lived sts tokens без hardcode -->
10
+ | SEC-008 | Insecure JWT Validation | `jwt.decode(token, options={"verify_signature": False})` | Проверять подпись, `issuer`, `audience`, `exp`, `nbf`, `alg` allowlist. | Cloud/Secrets | `CWE-347` | Проверять подпись, `issuer`, `audience`, `exp`, `nbf`, `alg` allowlist. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: sec-008 insecure jwt validation decode token options verify signature false проверять подпись issuer audience exp nbf alg allowlist -->
11
+ | SEC-009 | Vault Token in Plain Config | `vault_token: s.xxxxx` | Хранить Vault auth через AppRole/K8s auth + short-lived token, ротацию и scoped policies; обязательно использовать Vault Agent Injector для автоматической доставки и ротации токенов/секретов в workload. | Cloud/Secrets | `CWE-522` | Хранить Vault auth через AppRole/K8s auth + short-lived token, ротацию и scoped policies; обязательно использовать Vault Agent Injector для автоматической доставки и ротации токенов/секретов в workload. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: sec-009 vault token plain config vault agent injector approle k8s short lived rotation scoped policies -->
12
+ | SEC-010 | Vault TLS Verification Disabled | `vault_client = hvac.Client(url=VAULT_URL, verify=False)` | Всегда `verify=True`, mTLS/CA pinning, запрет insecure transport. | Cloud/Secrets | `CWE-295` | Всегда `verify=True`, mTLS/CA pinning, запрет insecure transport. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: sec-010 vault tls verification disabled client hvac url verify false всегда true mtls ca pinning запрет insecure transport -->
13
+ | SEC-011 | Unencrypted Secret in Object Storage | `s3.put_object(Bucket=b, Key=k, Body=secret_blob)` | Включить SSE-KMS/CMK, ограничить доступ bucket policy и включить audit trail. | Cloud/Secrets | `CWE-311` | Включить SSE-KMS/CMK, ограничить доступ bucket policy и включить audit trail. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: sec-011 unencrypted secret object storage s3 put bucket b key k body blob включить sse kms cmk ограничить доступ policy и -->
14
+ | SEC-012 | Broad KMS Permissions | `"Action": "kms:*", "Resource": "*"` | Принцип least privilege: ограничить actions/resources и контекст ключей. | Cloud/Secrets | `CWE-732` | Принцип least privilege: ограничить actions/resources и контекст ключей. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: sec-012 broad kms permissions action resource принцип least privilege ограничить actions resources и контекст ключей -->
15
+ | SEC-013 | Publicly Exposed Secrets Endpoint | `@app.get("/debug/secrets")`<br>`def dump(): return os.environ` | Удалить/закрыть debug endpoints, включить authz + environment gating для non-prod only. | Cloud/Secrets | `CWE-200` | Удалить/закрыть debug endpoints, включить authz + environment gating для non-prod only. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: sec-013 publicly exposed secrets endpoint app get debug def dump return os environ удалить закрыть endpoints включить authz environment gating для -->
16
+ | SEC-014 | Missing Secret Rotation Policy | `rotation_days = None` | Обязательная ротация секретов/ключей (TTL), автоматизация revoke/renew и контроль просрочки. | Cloud/Secrets | `CWE-613` | Обязательная ротация секретов/ключей (TTL), автоматизация revoke/renew и контроль просрочки. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: sec-014 missing secret rotation policy days none обязательная ротация секретов ключей ttl автоматизация revoke renew и контроль просрочки -->
17
+ | SEC-015 | Unsafe Secret in CI Variables | `echo $PROD_DB_PASSWORD` | Masked/protected CI variables, secret scanning в pipeline, запрет echo/print секретов. | Cloud/Secrets | `CWE-312` | Masked/protected CI variables, secret scanning в pipeline, запрет echo/print секретов. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: sec-015 unsafe secret ci variables echo prod db password masked protected scanning в pipeline запрет print секретов -->
18
+ | SEC-016 | External Secrets Operator Required | `kind: Secret`<br>`stringData:`<br>` password: plain-text` | Использовать `kind: ExternalSecret`, ссылающийся на `SecretStore/ClusterSecretStore` (Vault backend), и исключить хранение секретов в Git. | Cloud/Secrets | ESO + Vault best practices | Использовать `kind: ExternalSecret`, ссылающийся на `SecretStore/ClusterSecretStore` (Vault backend), и исключить хранение секретов в Git. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: sec-016 external secrets operator eso kind secret stringdata plain text secretstore vault -->
19
+ | SEC-017 | Trusted Mounts for DB Passwords | `env:`<br>` - name: DB_PASSWORD`<br>` value: supersecret` | Передавать секреты как файлы через `volumeMounts` (Vault Agent Injector или ESO synced volume), читать пароль из файловой системы, а не из ENV. | Cloud/Secrets | K8s secret delivery hardening | Передавать секреты как файлы через `volumeMounts` (Vault Agent Injector или ESO synced volume), читать пароль из файловой системы, а не из ENV. | Атакующий доставляет входные данные, соответствующие anti-pattern; реальный ущерб зависит от приёмника (sink), конфигурации и границ доверия. | <!-- semantic_anchor: sec-017 trusted mounts db password env volumemounts vault agent injector eso file based secrets -->
@@ -0,0 +1,28 @@
1
+ {
2
+ "skill_id": "cloud-secrets",
3
+ "name": "Cloud & Secrets",
4
+ "activation_triggers": [
5
+ "sec-imds-169254",
6
+ "sec-k8s-configmap-secret",
7
+ "sec-vault-token",
8
+ "sec-kms-policy-star",
9
+ "sec-env-leak-log",
10
+ "sec-eso-external-secret",
11
+ "sec-vault-agent-injector"
12
+ ],
13
+ "relevant_extensions": [
14
+ ".py",
15
+ ".yaml",
16
+ ".yml",
17
+ ".tf",
18
+ ".json"
19
+ ],
20
+ "tools": [
21
+ "semgrep",
22
+ "syft",
23
+ "trufflehog"
24
+ ],
25
+ "rules_path": "core/skills/cloud-secrets/patterns.md",
26
+ "few_shot_examples": "core/gold-standard-testbed/cloud_secrets_vulnerable.yaml",
27
+ "security_priority": 10
28
+ }
@@ -0,0 +1,103 @@
1
+ # C# / .NET
2
+
3
+ ## Stack overview
4
+
5
+ **ASP.NET** / .NET patterns: Roslyn, process execution, XML, cookies, crypto, and redirects. Metrics are prefixed **`CSH`**.
6
+
7
+ ## Top threats
8
+
9
+ - Code/command injection and unsafe reflection (`CSH-001`–`CSH-008`).
10
+ - Deserialization and XXE (`CSH-009`, `CSH-010`).
11
+ - Secrets, cookies, TLS (`CSH-011`–`CSH-015`, `CSH-016`).
12
+
13
+ ## Pattern catalog
14
+
15
+ Complete Anti-Pattern / Safe-Pattern definitions live in [`patterns.md`](patterns.md). The table below is a **table of contents** by metric ID.
16
+
17
+ | ID | Metric | Stack |
18
+ |---|---|---|
19
+ | `CSH-001` | C# Code Injection: `CSharpScript.EvaluateAsync` на пользовательском вводе | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
20
+ | `CSH-002` | Command Injection: `Process.Start` со строкой аргументов | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
21
+ | `CSH-003` | Shell Execute Injection: `UseShellExecute=true` с пользовательским вводом | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
22
+ | `CSH-004` | Unsafe Reflection: `Type.GetType` из user input | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
23
+ | `CSH-005` | Dynamic Invoke Injection: `GetMethod(...).Invoke` без allowlist | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
24
+ | `CSH-006` | SQL Fragment Injection в `ORDER BY` | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
25
+ | `CSH-007` | Roslyn Compilation of Untrusted Code | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
26
+ | `CSH-008` | JavaScript Engine Injection (Jint/ClearScript) | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
27
+ | `CSH-009` | Небезопасная десериализация | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
28
+ | `CSH-010` | XXE Injection | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
29
+ | `CSH-011` | Insecure Cookie Flags | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
30
+ | `CSH-012` | Hardcoded Secrets | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
31
+ | `CSH-013` | Weak Crypto | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
32
+ | `CSH-014` | Open Redirect | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
33
+ | `CSH-015` | Certificate Validation Bypass | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
34
+ | `CSH-016` | Weak Password Hashing | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
35
+ | `CSH-017` | Office HTML Injection в Outlook/Excel формулы | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
36
+ | `CSH-018` | VSTO macro-equivalent command execution | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
37
+ | `CSH-019` | Banned BinaryFormatter Deserialize | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
38
+ | `CSH-020` | Insecure DataSet.ReadXml from untrusted input | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
39
+ | `CSH-021` | Unsafe P/Invoke marshaling | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
40
+ | `CSH-022` | Insecure Assembly.Load from path/user input | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
41
+ | `CSH-023` | ASP.NET Mass Assignment (Entity binding) | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
42
+ | `CSH-024` | Unsafe AutoMapper profile exposing privileged fields | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
43
+ | `CSH-025` | JWT validation gaps in .NET auth | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
44
+ | `CSH-026` | OAuth redirect URI not validated | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
45
+ | `CSH-027` | Insecure file upload without extension/content checks | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
46
+ | `CSH-028` | Path traversal in static file/document download | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
47
+ | `CSH-029` | Missing anti-forgery on state-changing MVC actions | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
48
+ | `CSH-030` | Insecure session config in .NET 4.8 | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
49
+ | `CSH-031` | Json.NET TypeNameHandling unsafe mode | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
50
+ | `CSH-032` | ASP.NET request validation disabled | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
51
+ | `CSH-033` | Weak TLS protocol negotiation | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
52
+ | `CSH-034` | Insecure random via System.Random for secrets | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
53
+ | `CSH-035` | Sensitive data in logs/debug output | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
54
+ | `CSH-036` | LDAP injection via unescaped filter | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
55
+ | `CSH-037` | Regex DoS in server validation | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
56
+ | `CSH-038` | XML signature validation bypass | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
57
+ | `CSH-039` | gRPC auth metadata not validated | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
58
+ | `CSH-040` | GraphQL over-posting of sensitive fields | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
59
+ | `CSH-041` | Entity Framework FromSqlRaw injection | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
60
+ | `CSH-042` | Open telemetry export without data scrubbing | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
61
+ | `CSH-043` | WebClient legacy insecure usage | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
62
+ | `CSH-044` | Hardcoded service account credentials | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
63
+ | `CSH-045` | Missing object-level authorization in API | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
64
+ | `CSH-046` | Unsafe cleanup deletion with user-supplied path | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
65
+ | `CSH-047` | VSTO/.NET 4.8 unsafe `BinaryFormatter.Deserialize` usage | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
66
+ | `CSH-048` | Dynamic assembly loading from network paths (UNC/URL) | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
67
+ | `CSH-049` | SSRF C#: `HttpClient` к AWS metadata IP (CWE-918) | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
68
+ | `CSH-050` | SSRF C#: `WebRequest` к GCP metadata host (CWE-918) | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
69
+ | `CSH-051` | SSRF C#: `RestSharp` к link-local metadata (CWE-918) | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
70
+ | `CSH-052` | SSRF C#: `SocketsHttpHandler` без фильтра IMDS (CWE-918) | Use using/try-finally and safe .NET APIs; enforce strict allowlists for untrusted input. |
71
+ | `CSH-053` | Paladin: non-constant-time compare for password/token hash (CWE-613) | Replace `==` on secrets with `CryptographicOperations.FixedTimeEquals` or verified KDF APIs only. |
72
+ | `CSH-054` | Paladin: JWT `ValidateLifetime` disabled | Enable lifetime validation and align clock skew with token issuer SLA. |
73
+ | `CSH-055` | Paladin: JWT `ClockSkew` zeroed (clock tolerance removed) | Avoid `TimeSpan.Zero` unless IdP mandates; document skew rationale. |
74
+ | `CSH-056` | Paladin: `Path.GetTempFileName()` без явных ACL (CWE-377) | Create temp files under app-controlled dir with explicit ACL, not default shared temp. |
75
+ | `CSH-057` | Paladin: `Process.Start` путь с пробелами без кавычек (CWE-428) | Always quote/structure paths with spaces; avoid single-string overloads for untrusted paths. |
76
+ | `CSH-058` | Paladin: `Registry` ImagePath без кавычек при пробелах (CWE-428) | Quote service binary paths in registry; validate against allowlist. |
77
+
78
+ ## Verification
79
+
80
+ **Verification:** Check the gold testbed file(s) below for `Vulnerable: <ID>` markers (static Semgrep + `detection-matrix.md` ground truth).
81
+
82
+ - [`gold-standard-testbed/multi_lang_vulnerable/csharp_vulnerable.cs`](../gold-standard-testbed/multi_lang_vulnerable/csharp_vulnerable.cs)
83
+
84
+ After changing [`patterns.md`](patterns.md), run from the repo root:
85
+
86
+ ```bash
87
+ python scripts/sync_semgrep.py
88
+ ```
89
+
90
+ ## Workflow: Recon → Scan → Verify
91
+
92
+ ### 1) Recon
93
+ - Map entrypoints, data flows, and trust boundaries for this stack.
94
+ - Identify which metrics in [`patterns.md`](patterns.md) apply to the code under review.
95
+
96
+ ### 2) Scan
97
+ - Run Semgrep with `semgrep-rules/<skill>.yaml` (generated) and correlate with Anti-Patterns.
98
+ - Eliminate findings that cannot bind to a metric row.
99
+
100
+ ### 3) Verify
101
+ - Confirm markers or scanner hits for touched IDs in the gold testbed when adding metrics.
102
+ - Emit findings as `Vulnerable: <PREFIX>-<NNN>` in written reviews.
103
+