fix: A12–A24 — SSRF, auth routes legacy, uploads, N+1 et routes doublonnes (v7.3.3)
- A12 — `og_fetcher` : GET sans `follow_redirects`, `_is_public_host` revérifié à chaque saut (max 5) ; `POST /board/api/og/metadata` → 400 sur hôte privé/loopback - A13 — router automations sous `Depends(_require_session)` (CRUD, run, press-button) + `created_by` sans fallback ; action `webhook` validée par `_is_public_host` avant POST (SSRF) - A15 — webhooks sortants : `_require_admin` sur GET/POST/DELETE + `_is_public_host` sur l'URL en création - A17 — router legacy `/api` sous `Depends(_require_session_or_bearer)` (session ou Bearer `/api/v1`), allowlist explicite `/api/health` + `/api/frontend-error` - A22 — les 2 uploads locales : session exigée (`_require_user_id`) + `validate_upload` branché (taille + extension) + `FLOWDECK_DATA_DIR` au lieu de `/data` codé en dur - A23 — N+1 : COUNT→`GROUP BY` (dashboard), cards→`executemany` (board sync), duplicata de propriétés→`executemany` + remap des ids par SELECT (collections) - A24 — 2 routes écrasées supprimées : `GET /api/projects` (api.py) et `GET /workspace` (workspace.py) + test « aucun doublon méthode+chemin » - Tests : +9 dans `tests/test_audit_p0_fixes.py` (SSRF, 401s, validate_upload, doublons de routes) ; tests OG sur hôtes résolubles (la garde fait du DNS) - suite **1025/1025** · `ruff check app tests` OK
This commit is contained in:
@@ -912,9 +912,18 @@ async def list_workspace_projects(request: Request):
|
||||
rows = conn.execute(
|
||||
"SELECT id, title FROM pages WHERE parent_id IS NULL AND collection_row_id IS NULL ORDER BY updated_at DESC LIMIT 20"
|
||||
).fetchall()
|
||||
# A23 : une seule agrégation GROUP BY au lieu d'un COUNT par ligne.
|
||||
counts = {}
|
||||
if rows:
|
||||
for c in conn.execute(
|
||||
"SELECT parent_id, COUNT(*) AS c FROM pages WHERE parent_id IN ({}) GROUP BY parent_id".format(
|
||||
",".join("?" * len(rows))
|
||||
),
|
||||
[r["id"] for r in rows],
|
||||
).fetchall():
|
||||
counts[c["parent_id"]] = c["c"]
|
||||
for r in rows:
|
||||
count = conn.execute("SELECT COUNT(*) FROM pages WHERE parent_id=?", (r["id"],)).fetchone()[0]
|
||||
builtin.append({"id": str(r["id"]), "name": r["title"] or "Untitled", "pageCount": count, "forge": "builtin"})
|
||||
builtin.append({"id": str(r["id"]), "name": r["title"] or "Untitled", "pageCount": counts.get(r["id"], 0), "forge": "builtin"})
|
||||
|
||||
gitea_repos = []
|
||||
# Use per-user token if available, otherwise return empty
|
||||
@@ -1526,7 +1535,8 @@ async def upload_local_workspace_file(request: Request):
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ws = _get_active_workspace(request, user_id=_get_user_id(request))
|
||||
uid = _require_user_id(request) # A22 : pas d'upload anonyme
|
||||
ws = _get_active_workspace(request, user_id=uid)
|
||||
ws_id = ws["id"] if ws else None
|
||||
if not ws_id:
|
||||
return JSONResponse({"error": "No active workspace"}, status_code=400)
|
||||
@@ -1543,7 +1553,12 @@ async def upload_local_workspace_file(request: Request):
|
||||
if not files:
|
||||
return JSONResponse({"error": "No files provided"}, status_code=400)
|
||||
|
||||
upload_dir = Path(f"/data/uploads/workspace_{ws_id}")
|
||||
import os
|
||||
|
||||
from app.middleware.security import validate_upload
|
||||
|
||||
data_root = Path(os.environ.get("FLOWDECK_DATA_DIR", "/data"))
|
||||
upload_dir = data_root / f"uploads/workspace_{ws_id}"
|
||||
upload_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
results = []
|
||||
@@ -1564,10 +1579,14 @@ async def upload_local_workspace_file(request: Request):
|
||||
counter += 1
|
||||
|
||||
content = await f.read()
|
||||
err = validate_upload(safe_name, len(content)) # A22 : taille + extension
|
||||
if err:
|
||||
results.append({"name": safe_name, "error": err})
|
||||
continue
|
||||
file_path.write_bytes(content)
|
||||
|
||||
# Determine if this is a folder marker or actual file
|
||||
rel_path = str(file_path.relative_to("/data"))
|
||||
rel_path = str(file_path.relative_to(data_root))
|
||||
size = len(content)
|
||||
mime = f.content_type or "application/octet-stream"
|
||||
|
||||
@@ -1599,7 +1618,8 @@ async def upload_local_workspace_folder(request: Request):
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ws = _get_active_workspace(request, user_id=_get_user_id(request))
|
||||
uid = _require_user_id(request) # A22 : pas d'upload anonyme
|
||||
ws = _get_active_workspace(request, user_id=uid)
|
||||
ws_id = ws["id"] if ws else None
|
||||
if not ws_id:
|
||||
return JSONResponse({"error": "No active workspace"}, status_code=400)
|
||||
@@ -1621,7 +1641,12 @@ async def upload_local_workspace_folder(request: Request):
|
||||
except json.JSONDecodeError:
|
||||
return JSONResponse({"error": "Invalid structure JSON"}, status_code=400)
|
||||
|
||||
upload_dir = Path(f"/data/uploads/workspace_{ws_id}")
|
||||
import os
|
||||
|
||||
from app.middleware.security import validate_upload
|
||||
|
||||
data_root = Path(os.environ.get("FLOWDECK_DATA_DIR", "/data"))
|
||||
upload_dir = data_root / f"uploads/workspace_{ws_id}"
|
||||
upload_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
results = []
|
||||
@@ -1680,9 +1705,13 @@ async def upload_local_workspace_folder(request: Request):
|
||||
counter += 1
|
||||
|
||||
content = await matched.read()
|
||||
err = validate_upload(safe_name, len(content)) # A22 : taille + extension
|
||||
if err:
|
||||
results.append({"name": safe_name, "error": err})
|
||||
continue
|
||||
file_path.write_bytes(content)
|
||||
|
||||
rel_path = str(file_path.relative_to("/data"))
|
||||
rel_path = str(file_path.relative_to(data_root))
|
||||
cursor = conn.execute(
|
||||
"""INSERT INTO pages (workspace, workspace_id, title, content, content_format, parent_section, parent_id)
|
||||
VALUES ('', ?, ?, ?, 'file', 'Private', ?)""",
|
||||
|
||||
Reference in New Issue
Block a user