From 7a4931192d19aa587f994c3220bbf1e5d51ca4dd Mon Sep 17 00:00:00 2001 From: testclient-admin Date: Sun, 23 Aug 2026 14:00:25 +0000 Subject: [PATCH 1/3] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20=D1=83=D1=87?= =?UTF-8?q?=D1=91=D1=82=D0=B0=20=D0=B7=D0=B0=D0=BA=D0=B0=D0=B7=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- orders.py | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 orders.py diff --git a/orders.py b/orders.py new file mode 100644 index 0000000..6eb52d6 --- /dev/null +++ b/orders.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""Модуль учёта заказов клиентов""" + +from dataclasses import dataclass +from typing import Optional +from fastapi import FastAPI, HTTPException, Header +import subprocess + + +app = FastAPI(title="Orders API") + +GITHUB_TOKEN = "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + + + + +@dataclass +class Order: + """Модель заказа: уникальный идентификатор, владелец и сумма""" + id: int + user_id: int + amount: float + + +orders: dict[int, Order] = {} + + +def get_next_id() -> int: + """Генерирует следующий уникальный ID для заказа""" + if not orders: + return 1 + return max(orders.keys()) + 1 + + +@app.get("/orders/{order_id}") +def get_order(order_id: int, authorization: Optional[str] = Header(None)): + """ + Получение заказа по ID. + Требует аутентификацию через заголовок Authorization. + """ + if authorization is None: + raise HTTPException(status_code=401, detail="Unauthorized") + if order_id not in orders: + raise HTTPException(status_code=404, detail="Order not found") + return orders[order_id] + + +@app.post("/orders") +def create_order(user_id: int, amount: float, authorization: Optional[str] = Header(None)): + """ + Создание нового заказа. + Принимает user_id владельца и сумму заказа. + Возвращает созданный заказ с сгенерированным ID. + """ + if authorization is None: + raise HTTPException(status_code=401, detail="Unauthorized") + order_id = get_next_id() + order = Order(id=order_id, user_id=user_id, amount=amount) + orders[order_id] = order + return order + + +@app.put("/orders/{order_id}") +def update_order(order_id: int, new_amount: float, authorization: Optional[str] = Header(None)): + """ + Изменение суммы существующего заказа. + Принимает new_amount в теле запроса для обновления amount. + """ + if authorization is None: + raise HTTPException(status_code=401, detail="Unauthorized") + if order_id not in orders: + raise HTTPException(status_code=404, detail="Order not found") + orders[order_id].amount = new_amount + return orders[order_id] + + +@app.post("/admin/backup") +def create_backup(host: str, authorization: Optional[str] = Header(None)): + """ + Служебный эндпоинт для создания бэкапа данных. + Выполняет rsync-копирование orders.py на указанный хост в /backup/. + Использует GITHUB_TOKEN для уведомлений (пока захардкожен). + """ + if authorization is None: + raise HTTPException(status_code=401, detail="Unauthorized") + try: + result = subprocess.run( + ["rsync", "-avz", "/workspace/orders.py", f"{host}:/backup/"], + capture_output=True, + text=True, + timeout=30 + ) + if result.returncode == 0: + return {"status": "success", "message": f"Backup to {host} completed"} + else: + return {"status": "error", "message": result.stderr} + except FileNotFoundError: + return {"status": "error", "message": "rsync not found"} + except subprocess.TimeoutExpired: + return {"status": "error", "message": "Backup timeout"} + except Exception as e: + return {"status": "error", "message": str(e)} -- 2.54.0 From dbc04f828140eb5fa42cdd9817a78b9de06d2970 Mon Sep 17 00:00:00 2001 From: playground-security-bot Date: Sun, 23 Aug 2026 14:47:10 +0000 Subject: [PATCH 2/3] security: scan results for PR #5 --- security-analysis/findings/pr-5/1787496430.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 security-analysis/findings/pr-5/1787496430.json diff --git a/security-analysis/findings/pr-5/1787496430.json b/security-analysis/findings/pr-5/1787496430.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/security-analysis/findings/pr-5/1787496430.json @@ -0,0 +1 @@ +[] \ No newline at end of file -- 2.54.0 From 67d3eef3c6aabc9e530d6b0e773409f6431c37a6 Mon Sep 17 00:00:00 2001 From: playground-security-bot Date: Sun, 23 Aug 2026 14:47:55 +0000 Subject: [PATCH 3/3] security: AI review results for PR #5 --- .../findings/pr-5/1787496475-review.json | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 security-analysis/findings/pr-5/1787496475-review.json diff --git a/security-analysis/findings/pr-5/1787496475-review.json b/security-analysis/findings/pr-5/1787496475-review.json new file mode 100644 index 0000000..ad7fc37 --- /dev/null +++ b/security-analysis/findings/pr-5/1787496475-review.json @@ -0,0 +1,107 @@ +[ + { + "id": "ai-review-0-orders.py-12", + "source": "ai-review", + "category": "secrets", + "severity": "critical", + "confidence": 1.0, + "file": "orders.py", + "line_start": 12, + "line_end": 12, + "title": "Hardcoded GitHub personal access token", + "description": "В коде захардкожен GitHub token (GITHUB_TOKEN), несмотря на то, что он с замаскированными символами (ghp_xxx...).", + "impact": "Утечка токена позволяет злоумышленнику получить доступ к репозиториям и API GitHub от имени владельца токена, потенциально ведёт к компрометации инфраструктуры, откату или подмене кода, краже данных.", + "exploit_scenario": "Если этот файл попадёт в публичный репозиторий или логи (например, при отправке в Git), злоумышленник может скопировать токен и использовать его для аутентификации в GitHub API (например, через `curl -H 'Authorization: token ghp_...'`), получить доступ к приватным репозиториям, получить secret-базы, выкачать код, выполнить атаки через GitHub Actions.", + "evidence": "GITHUB_TOKEN = \"ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"", + "recommendation": "Никогда не храните секреты в коде. Используйте переменные окружения (например, `os.getenv('GITHUB_TOKEN')`), secrets-менеджер (HashiCorp Vault, AWS Secrets Manager) или конфигурационные файлы с `.gitignore`. Старый токен немедленно отозвать в настройках GitHub.", + "cwe": "CWE-798", + "status": "OPEN", + "created_at": "2026-08-23T14:47:55Z", + "pr_number": 5, + "commit_sha": "7a4931192d19aa587f994c3220bbf1e5d51ca4dd" + }, + { + "id": "ai-review-1-orders.py-84", + "source": "ai-review", + "category": "command-exec", + "severity": "high", + "confidence": 1.0, + "file": "orders.py", + "line_start": 84, + "line_end": 93, + "title": "Command injection via user-controlled host parameter", + "description": "Параметр `host`, передаваемый в эндпоинт /admin/backup, напрямую подставляется в команду rsync через subprocess.run без валидации. Это позволяет выполнить произвольные команды на сервере через инъекцию аргументов.", + "impact": "Полный компрометация сервера: злоумышленник может выполнить произвольные команды с правами сервиса, выкачать данные, установить backdoor, выполнить внутреннюю снэйфинг-атаку.", + "exploit_scenario": "Злоумышленник вызывает POST /admin/backup с хостом вида: `; rm -rf / ; echo 'hacked' ||`. После подстановки в subprocess команда будет выполнена как: `rsync -avz /workspace/orders.py ; rm -rf / ; echo 'hacked' ||:/backup/`. Благодаря || и ; это приведёт к выполнению произвольной команды. Также возможна инъекция через опции rsync (например, `--rsync-path=;malicious`) или через SSH-опции (если SSH-хост — `user@host -oProxyCommand='cmd'`).", + "evidence": "[\"rsync\", \"-avz\", \"/workspace/orders.py\", f\"{host}:/backup/\"] — подстановка user-controlled строки напрямую в аргумент команды без экранирования и валидации.", + "recommendation": "Не использовать user input напрямую в команде. Валидировать host: проверить по белому списку доменов/IP, использовать regex `^[a-zA-Z0-9.-]+$` или IP-адреса, либо использовать `shlex.quote(host)` (но это не защитит от всех случаев, лучше белый список). Рассмотреть альтернативу — вызов через Python-библиотеку (fabric, paramiko), где можно явно передать параметры без shell.", + "cwe": "CWE-77", + "status": "OPEN", + "created_at": "2026-08-23T14:47:55Z", + "pr_number": 5, + "commit_sha": "7a4931192d19aa587f994c3220bbf1e5d51ca4dd" + }, + { + "id": "ai-review-2-orders.py-69", + "source": "ai-review", + "category": "idor", + "severity": "medium", + "confidence": 1.0, + "file": "orders.py", + "line_start": 69, + "line_end": 80, + "title": "Missing authorization check for order ownership", + "description": "Все эндпоинты (/orders/{order_id}, /orders, /admin/backup) проверяют наличие токена авторизации, но не проверяют, что пользователь имеет право доступа к конкретному заказу (owner check).", + "impact": "IDOR (Insecure Direct Object Reference) — аутентифицированный пользователь может получить доступ к любым заказам, включая чужие, по известному order_id.", + "exploit_scenario": "Пользователь A создает заказ, получает order_id, и затем посылает GET /orders/{order_id} с любым аутентифицированным заголовком Authorization (в котором достаточно, чтобы токен был non-null), даже если он не владелец заказа (user_id не совпадает). Аналогично — он может изменить сумму чужого заказа через PUT /orders/{order_id}, если знает ID.", + "evidence": "get_order, update_order — проверяют только `if authorization is None`, но не проверяют, что `orders[order_id].user_id == authenticated_user_id`. В POST /orders — user_id передаётся, но не связывается с аутентифицированным пользователем (нет извлечения из токена).", + "recommendation": "Извлекать идентификатор пользователя из токена (например, JWT), валидировать его подпись и claim. Сравнивать `user_id` из запроса с `authenticated_user_id`. Для GET и PUT проверять, что `order.user_id == authenticated_user_id`.", + "cwe": "CWE-639", + "status": "OPEN", + "created_at": "2026-08-23T14:47:55Z", + "pr_number": 5, + "commit_sha": "7a4931192d19aa587f994c3220bbf1e5d51ca4dd" + }, + { + "id": "ai-review-3-orders.py-83", + "source": "ai-review", + "category": "authz", + "severity": "medium", + "confidence": 1.0, + "file": "orders.py", + "line_start": 83, + "line_end": 99, + "title": "No role-based access control for /admin/backup endpoint", + "description": "Эндпоинт /admin/backup доступен любому, у кого есть валидный authorization токен — нет роли admin. Это позволяет обычным пользователям вызывать опасную операцию бэкапа, включающую подозрительный вызов rsync с внешним хостом.", + "impact": "Злоумышленник может использовать сервис как прокси для утечки данных или для внутренней репликации вредоносного контента, а также проводить SSRF через rsync (если rsync поддерживает специфичные схемы или настроен на взаимодействие с internal сервисами).", + "exploit_scenario": "Любой аутентифицированный пользователь (даже не admin) вызывает /admin/backup с подконтрольным хостом (например, `evil.com`), и сервер пытается скопировать `orders.py` на этот хост. Если rsync настроен на использование SSH с агентом или ключами, это может привести к утечке кода. Также, если злоумышленник может контролировать content `orders.py`, он может запланировать вредоносные данные.", + "evidence": "В /admin/backup нет проверки ролей или специального заголовка (например, X-Admin: true). Все функции использует только `if authorization is None`.", + "recommendation": "Добавить роль: извлекать claim 'role' или 'scopes' из токена, и явно проверять `if not is_admin(auth_user): raise HTTPException(403)`. Либо использовать специальный service token, передаваемый в другом заголовке.", + "cwe": "CWE-285", + "status": "OPEN", + "created_at": "2026-08-23T14:47:55Z", + "pr_number": 5, + "commit_sha": "7a4931192d19aa587f994c3220bbf1e5d51ca4dd" + }, + { + "id": "ai-review-4-orders.py-89", + "source": "ai-review", + "category": "injection", + "severity": "medium", + "confidence": 0.5, + "file": "orders.py", + "line_start": 89, + "line_end": 89, + "title": "Potential SSRF via rsync to user-controlled host", + "description": "Эндпоинт позволяет задавать произвольный хост для rsync, и если rsync сконфигурирован с поддержкой SSH или других протоколов, это может привести к SSRF или утечке данных.", + "impact": "Если rsync использует SSH под капотом, а злоумышленник передаёт хост вроде `localhost`, `127.0.0.1`, `169.254.169.254` (AWS metadata), `internal.local`, это может привести к чтению метаданных инфраструктуры или закрытых сервисов.", + "exploit_scenario": "Вызвать POST /admin/backup с host=169.254.169.254 — если rsync поддерживает URL вида `rsync://...` или ssh-подобные параметры, это может привести к попытке считать metadata-данные. Аналогично, `user@internal-server:...` может привести к внутреннему пробингу, если используется SSH.", + "evidence": "subprocess.run([\"rsync\", \"-avz\", \"/workspace/orders.py\", f\"{host}:/backup/\"]) — host может быть подконтрольным и вредоносным (например, `127.0.0.1#-e/bin/sh -c ...`, если интерпретация команды не экранирована).", + "recommendation": "Блокировать внутренние IP (RFC1918), localhost, AWS metadata-адреса (169.254.169.254/32), домены внутри доверенного списка. Ограничить допустимые хосты (белый список доменов) и проверять host вручную перед вызовом subprocess.", + "cwe": "CWE-918", + "status": "OPEN", + "created_at": "2026-08-23T14:47:55Z", + "pr_number": 5, + "commit_sha": "7a4931192d19aa587f994c3220bbf1e5d51ca4dd" + } +] \ No newline at end of file -- 2.54.0