forked from enderofwings/NexusOS
feat(security): request-size caps, concurrency limits, model-pull allowlist
DoS/quota guardrails for the unauthenticated local APIs: * Body-size middleware rejects oversized requests (Content-Length) before they are buffered/base64-decoded (NEXUS_MAX_REQUEST_MB, default 32). * Document upload enforces a decoded-byte cap (NEXUS_MAX_UPLOAD_MB, default 20) and a PDF page-count cap (NEXUS_MAX_PDF_PAGES, default 500) as backstops for chunked bodies and pathological files. * A counter-based in-flight limiter bounds concurrent chats and document ingests (NEXUS_MAX_CONCURRENT_CHATS/UPLOADS), returning 429 when saturated; the chat slot is held for the whole SSE stream and released on completion or client disconnect. * /models/pull gains an opt-in allowlist (NEXUS_MODEL_ALLOWLIST); empty by default so behaviour is unchanged, otherwise a bare repo name permits all its tags. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+40
-1
@@ -166,6 +166,42 @@ _LOCAL_ORIGINS = [
|
||||
ALLOWED_HOSTS = _csv_env("NEXUS_ALLOWED_HOSTS", _LOCAL_HOSTS)
|
||||
ALLOWED_ORIGINS = _csv_env("NEXUS_ALLOWED_ORIGINS", _LOCAL_ORIGINS)
|
||||
|
||||
|
||||
# --- resource limits (DoS guardrails for the unauthenticated local APIs) ---
|
||||
# Even local-only, an unbounded base64 upload or a flood of concurrent inference
|
||||
# requests can exhaust RAM/CPU. These are generous defaults for single-user use,
|
||||
# all env-overridable.
|
||||
def _int_env(name: str, default: int) -> int:
|
||||
try:
|
||||
return int((os.getenv(name) or "").strip() or default)
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
MAX_REQUEST_BYTES = _int_env("NEXUS_MAX_REQUEST_MB", 32) * 1024 * 1024
|
||||
MAX_UPLOAD_BYTES = _int_env("NEXUS_MAX_UPLOAD_MB", 20) * 1024 * 1024
|
||||
MAX_PDF_PAGES = _int_env("NEXUS_MAX_PDF_PAGES", 500)
|
||||
MAX_CONCURRENT_CHATS = _int_env("NEXUS_MAX_CONCURRENT_CHATS", 4)
|
||||
MAX_CONCURRENT_UPLOADS = _int_env("NEXUS_MAX_CONCURRENT_UPLOADS", 2)
|
||||
|
||||
# Opt-in allowlist for /models/pull. Empty (default) = unrestricted, preserving
|
||||
# current behaviour. Set NEXUS_MODEL_ALLOWLIST=mistral,llama3 to bound which
|
||||
# models can be downloaded; a bare repo name (before the ':tag') matches all of
|
||||
# its tags, so "mistral" permits "mistral:latest", "mistral:7b", etc.
|
||||
MODEL_ALLOWLIST = _csv_env("NEXUS_MODEL_ALLOWLIST", [])
|
||||
|
||||
|
||||
def model_pull_allowed(name: str) -> bool:
|
||||
"""True if `name` may be pulled: always when no allowlist is configured,
|
||||
otherwise when the full name or its repo part (before the first ':') is
|
||||
listed. Case-insensitive."""
|
||||
if not MODEL_ALLOWLIST:
|
||||
return True
|
||||
n = (name or "").strip().lower()
|
||||
if not n:
|
||||
return False
|
||||
allow = {a.lower() for a in MODEL_ALLOWLIST}
|
||||
return n in allow or n.split(":", 1)[0] in allow
|
||||
|
||||
# exported instance
|
||||
settings = Settings()
|
||||
|
||||
@@ -176,7 +212,10 @@ __all__ = ["Settings", "settings", "path", "VERSION",
|
||||
"MEMORY_DIR", "LOGS_DIR", "PLAYBOOK_DIR", "UPLOADS_DIR",
|
||||
"EXPORTS_DIR", "MEMORY_DB",
|
||||
"BACKEND_LOG", "OLLAMA_LOG", "CHAT_LOG",
|
||||
"ALLOWED_HOSTS", "ALLOWED_ORIGINS"]
|
||||
"ALLOWED_HOSTS", "ALLOWED_ORIGINS",
|
||||
"MAX_REQUEST_BYTES", "MAX_UPLOAD_BYTES", "MAX_PDF_PAGES",
|
||||
"MAX_CONCURRENT_CHATS", "MAX_CONCURRENT_UPLOADS",
|
||||
"MODEL_ALLOWLIST", "model_pull_allowed"]
|
||||
|
||||
# --- quick runtime sanity check when run directly (no side effects on import) ---
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user