feat(settings): allowlist /api/settings keys (v1.7.1)
This commit is contained in:
16
app.py
16
app.py
@@ -55,7 +55,7 @@ syslog_handler.setFormatter(
|
|||||||
log.addHandler(syslog_handler)
|
log.addHandler(syslog_handler)
|
||||||
|
|
||||||
# --- Configuration ---
|
# --- Configuration ---
|
||||||
VERSION = "1.7.0"
|
VERSION = "1.7.1"
|
||||||
OLLAMA_BASE = "http://localhost:11434"
|
OLLAMA_BASE = "http://localhost:11434"
|
||||||
SEARXNG_BASE = "http://localhost:8888"
|
SEARXNG_BASE = "http://localhost:8888"
|
||||||
BASE_DIR = Path(__file__).parent
|
BASE_DIR = Path(__file__).parent
|
||||||
@@ -102,6 +102,12 @@ MAX_PRESET_PROMPT_CHARS = 12000
|
|||||||
MAX_SETTINGS_KEYS = 16
|
MAX_SETTINGS_KEYS = 16
|
||||||
MAX_SETTINGS_VALUE_CHARS = 8000
|
MAX_SETTINGS_VALUE_CHARS = 8000
|
||||||
MAX_CONVERSATION_TITLE_CHARS = 200
|
MAX_CONVERSATION_TITLE_CHARS = 200
|
||||||
|
ALLOWED_SETTINGS_KEYS = {
|
||||||
|
"profile_enabled",
|
||||||
|
"default_model",
|
||||||
|
"search_enabled",
|
||||||
|
"memory_enabled",
|
||||||
|
}
|
||||||
|
|
||||||
# --- Templates and Static Files ---
|
# --- Templates and Static Files ---
|
||||||
templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
|
templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
|
||||||
@@ -1439,6 +1445,14 @@ async def update_settings(request: Request):
|
|||||||
raise HTTPException(status_code=400, detail="Settings payload must be an object")
|
raise HTTPException(status_code=400, detail="Settings payload must be an object")
|
||||||
if len(body) > MAX_SETTINGS_KEYS:
|
if len(body) > MAX_SETTINGS_KEYS:
|
||||||
raise HTTPException(status_code=413, detail="Too many settings in one request")
|
raise HTTPException(status_code=413, detail="Too many settings in one request")
|
||||||
|
unknown_keys = sorted(
|
||||||
|
key for key in body.keys() if str(key) not in ALLOWED_SETTINGS_KEYS
|
||||||
|
)
|
||||||
|
if unknown_keys:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Unknown setting key(s): {', '.join(unknown_keys)}",
|
||||||
|
)
|
||||||
db = get_db()
|
db = get_db()
|
||||||
for key, value in body.items():
|
for key, value in body.items():
|
||||||
if len(str(key)) > 80 or len(str(value)) > MAX_SETTINGS_VALUE_CHARS:
|
if len(str(key)) > 80 or len(str(value)) > MAX_SETTINGS_VALUE_CHARS:
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Total identified items: 26
|
|||||||
2. [P0][DONE] Add CSRF/origin protection for browser-initiated state-changing requests.
|
2. [P0][DONE] Add CSRF/origin protection for browser-initiated state-changing requests.
|
||||||
3. [P0][DONE] Block unsafe URL schemes in rendered search-result links (e.g., javascript:).
|
3. [P0][DONE] Block unsafe URL schemes in rendered search-result links (e.g., javascript:).
|
||||||
4. [P0][DONE] Add rate limiting and request body size limits for chat/search/profile APIs.
|
4. [P0][DONE] Add rate limiting and request body size limits for chat/search/profile APIs.
|
||||||
5. [P1] Restrict settings updates to an allowlist of valid keys.
|
5. [P1][DONE] Restrict settings updates to an allowlist of valid keys.
|
||||||
6. [P1] Add pagination + hard caps on list endpoints (memories, conversations, message history).
|
6. [P1] Add pagination + hard caps on list endpoints (memories, conversations, message history).
|
||||||
7. [P1] Stop returning raw exception text to clients; use safe error envelopes.
|
7. [P1] Stop returning raw exception text to clients; use safe error envelopes.
|
||||||
8. [P1] Add automated tests for chat streaming, auto-search trigger, and memory command paths.
|
8. [P1] Add automated tests for chat streaming, auto-search trigger, and memory command paths.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# ⚡ JarvisChat v1.7.0
|
# ⚡ JarvisChat v1.7.1
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ Top 10 (brief):
|
|||||||
2. P0 [DONE]: Add CSRF/origin protection for state-changing requests
|
2. P0 [DONE]: Add CSRF/origin protection for state-changing requests
|
||||||
3. P0 [DONE]: Block unsafe URL schemes in rendered links
|
3. P0 [DONE]: Block unsafe URL schemes in rendered links
|
||||||
4. P0 [DONE]: Add rate limiting and request size limits
|
4. P0 [DONE]: Add rate limiting and request size limits
|
||||||
5. P1: Restrict `/api/settings` updates to allowlisted keys
|
5. P1 [DONE]: Restrict `/api/settings` updates to allowlisted keys
|
||||||
6. P1: Add pagination + hard caps for list APIs
|
6. P1: Add pagination + hard caps for list APIs
|
||||||
7. P1: Replace raw exception leakage with safe client errors
|
7. P1: Replace raw exception leakage with safe client errors
|
||||||
8. P1: Add automated tests for streaming/search/memory paths
|
8. P1: Add automated tests for streaming/search/memory paths
|
||||||
|
|||||||
57
tests/test_settings_allowlist.py
Normal file
57
tests/test_settings_allowlist.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
import app as app_module
|
||||||
|
|
||||||
|
|
||||||
|
def make_admin_client(tmp_path: Path) -> tuple[TestClient, dict[str, str]]:
|
||||||
|
os.environ["JARVISCHAT_ADMIN_PIN"] = "1234"
|
||||||
|
app_module.DB_PATH = tmp_path / "jarvischat-settings.db"
|
||||||
|
app_module.SESSIONS.clear()
|
||||||
|
app_module.PIN_ATTEMPTS.clear()
|
||||||
|
app_module.init_db()
|
||||||
|
|
||||||
|
client = TestClient(app_module.app)
|
||||||
|
login = client.post(
|
||||||
|
"/api/auth/login",
|
||||||
|
json={"pin": "1234"},
|
||||||
|
headers={"Origin": "http://testserver"},
|
||||||
|
)
|
||||||
|
assert login.status_code == 200
|
||||||
|
sid = login.json()["session_id"]
|
||||||
|
headers = {"X-Session-ID": sid, "Origin": "http://testserver"}
|
||||||
|
return client, headers
|
||||||
|
|
||||||
|
|
||||||
|
def test_settings_allow_known_keys(tmp_path: Path):
|
||||||
|
client, headers = make_admin_client(tmp_path)
|
||||||
|
try:
|
||||||
|
resp = client.put(
|
||||||
|
"/api/settings",
|
||||||
|
json={
|
||||||
|
"profile_enabled": "false",
|
||||||
|
"search_enabled": "true",
|
||||||
|
"memory_enabled": "false",
|
||||||
|
"default_model": "llama3.1:latest",
|
||||||
|
},
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
finally:
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_settings_reject_unknown_keys(tmp_path: Path):
|
||||||
|
client, headers = make_admin_client(tmp_path)
|
||||||
|
try:
|
||||||
|
resp = client.put(
|
||||||
|
"/api/settings",
|
||||||
|
json={"admin_pin_hash": "oops"},
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
assert resp.status_code == 400
|
||||||
|
assert "Unknown setting key" in resp.json().get("detail", "")
|
||||||
|
finally:
|
||||||
|
client.close()
|
||||||
Reference in New Issue
Block a user