FIX: 11 bugs bảo mật/ổn định (static mount chặn dotfile, delete traversal, cleanup giữ clips serverFileId, upload whitelist, password strength, auth audio endpoints, pedalboard==0.9.19, vendor CDN local) + FEATURE: Carla bridge preview/export MIDI notes âm VSTi (POST /midi-render, /carla-play-notes, nút Preview VSTi/Export MIDI->Audio; pedalboard 0.9.19 raw MIDI bytes; SONICFORGE_STORAGE_DIR cô lập test storage)

This commit is contained in:
2026-08-10 18:46:54 +07:00
parent 67ad2b8e4b
commit 61cb4b846f
33 changed files with 1097 additions and 103 deletions
+126 -3
View File
@@ -10,7 +10,9 @@ Covers the vulnerabilities found during the 2026-08 audit:
"""
import os
import json
import time
import httpx
import numpy as np
import pytest
import soundfile as sf
@@ -68,9 +70,32 @@ class TestAIProxySSRF:
def test_proxy_allows_configured_localhost_provider(self):
# localhost:11434 is in the default AI provider list; it must pass the
# SSRF check (and then fail to connect in this environment -> 502).
resp = client.post("/api/v1/ai/proxy", headers=auth_headers(),
json={"url": "http://localhost:11434/v1/chat/completions", "body": {}})
# SSRF check (and then fail to connect -> 502). Bug #5: máy có Ollama
# chạy ở localhost:11434 trả 200 → test fail giả. Mock connection fail
# để test deterministic, không phụ thuộc service ngoài.
import app.api.v1.ai_proxy as ai_proxy
class _FakeConnectError(httpx.ConnectError):
def __init__(self, *a, **k):
super().__init__("mock connection refused", request=httpx.Request("POST", "http://localhost:11434/"))
class _FakeAsyncClient(httpx.AsyncClient):
async def __aenter__(self):
return self
async def __aexit__(self, *a):
return False
async def post(self, *a, **k):
raise _FakeConnectError()
monkeypatch = pytest.MonkeyPatch()
monkeypatch.setattr(ai_proxy.httpx, "AsyncClient", _FakeAsyncClient)
try:
resp = client.post("/api/v1/ai/proxy", headers=auth_headers(),
json={"url": "http://localhost:11434/v1/chat/completions", "body": {}})
finally:
monkeypatch.undo()
assert resp.status_code == 502
@@ -232,3 +257,101 @@ class TestRenderResample:
os.remove(src_path)
except OSError:
pass
# ── 7. Bug fixes 2026-08-10 audit round 2 ──
class TestBugFixes:
def test_static_mount_blocks_dotfiles(self):
# Bug #1: /static/audio/.secret_key, sonicforge.db, temp/autosave.json
# từng serve 200 không cần auth — giờ phải 404.
assert client.get("/static/audio/.secret_key").status_code == 404
assert client.get("/static/audio/sonicforge.db").status_code == 404
assert client.get("/static/audio/temp/autosave.json").status_code == 404
assert client.get("/static/audio/../.secret_key").status_code in (404, 400)
def test_static_uploads_still_served(self):
# File audio hợp lệ trong uploads vẫn phải serve được (không vỡ UI).
fid = f"user_test_{os.urandom(4).hex()}.wav"
p = os.path.join(settings.UPLOADS_DIR, fid)
with open(p, "wb") as f:
f.write(b"RIFFxxxxWAVE")
try:
assert client.get(f"/static/audio/uploads/{fid}").status_code == 200
finally:
os.remove(p)
def test_delete_user_file_blocks_traversal(self):
# Bug #2: `user_<id>_../../x` không được xóa file ngoài storage.
token = get_admin_token()
if not token:
pytest.skip("Cannot get admin token")
prof = client.get("/api/v1/auth/profile", headers={"Authorization": f"Bearer {token}"})
uid = prof.json()["id"]
marker = os.path.join(settings.STORAGE_DIR, "pwn_marker_%s.txt" % os.urandom(4).hex())
with open(marker, "w") as f:
f.write("x")
try:
resp = client.delete(f"/api/v1/audio/my-files/user_{uid}_../../{os.path.basename(marker)}",
headers={"Authorization": f"Bearer {token}"})
assert resp.status_code in (403, 404)
assert os.path.exists(marker), "traversal delete phải bị chặn"
finally:
if os.path.exists(marker):
os.remove(marker)
def test_upload_rejects_non_audio_extension(self):
# Bug #4: upload .exe bị từ chối.
resp = client.post("/api/v1/audio/upload",
files={"file": ("evil.exe", b"MZ\x90\x00", "application/octet-stream")})
assert resp.status_code == 400
def test_change_password_rejects_weak(self):
# Bug #7: change-password áp cùng policy độ mạnh như register.
token = get_admin_token()
if not token:
pytest.skip("Cannot get admin token")
resp = client.post("/api/v1/auth/change-password",
headers={"Authorization": f"Bearer {token}"},
json={"old_password": "admin123", "new_password": "a"})
assert resp.status_code == 400
def test_cleanup_keeps_referenced_files(self):
# Bug #3: cleanup_expired_files_task không xóa file được project tham chiếu.
from app.tasks.worker import cleanup_expired_files_task
from app.models.user import get_db_connection
token = get_admin_token()
if not token:
pytest.skip("Cannot get admin token")
prof = client.get("/api/v1/auth/profile", headers={"Authorization": f"Bearer {token}"})
uid = prof.json()["id"]
ref_id = f"user_{uid}_ref_{os.urandom(4).hex()}.wav"
stray_id = f"user_{uid}_stray_{os.urandom(4).hex()}.wav"
ref_path = os.path.join(settings.PROCESSED_DIR, ref_id)
stray_path = os.path.join(settings.PROCESSED_DIR, stray_id)
old = time.time() - 999999
for p in (ref_path, stray_path):
with open(p, "wb") as f:
f.write(b"RIFFxxxxWAVE")
os.utime(p, (old, old))
pid = f"cleanup_test_{os.urandom(4).hex()}"
conn = get_db_connection()
cursor = conn.cursor()
proj = {"tracks": [{"serverFileId": ref_id}]}
cursor.execute("INSERT INTO projects (id, user_id, name, data_json, is_temp, size_bytes, updated_at) VALUES (?,?,?,?,0,?,?)",
(pid, uid, "cleanup test", json.dumps(proj), 0, old))
conn.commit()
conn.close()
try:
result = cleanup_expired_files_task(max_age_hours=0)
assert os.path.exists(ref_path), "file được project tham chiếu phải giữ"
assert not os.path.exists(stray_path), "file không tham chiếu phải bị dọn"
assert result["referenced_files_kept"] >= 1
finally:
for p in (ref_path, stray_path):
if os.path.exists(p):
os.remove(p)
conn = get_db_connection()
cur = conn.cursor()
cur.execute("DELETE FROM projects WHERE id = ?", (pid,))
conn.commit()
conn.close()