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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user