Please report security issues privately through GitHub Security Advisories rather than opening a public issue.
We aim to acknowledge a report within 3 working days and to agree a disclosure timeline with you once the issue is confirmed.
Useful details: affected version, reproduction steps, and what an attacker could achieve.
Lasagna ingests untrusted documents, calls external model providers, and can expose a REST API and MCP server. Reports we are particularly interested in:
- Document parsing that escapes its sandbox or achieves code execution
- Cross-tenant or cross-session leakage of documents, chunks or configuration
- Authentication or authorization bypass on the REST API or MCP server
- Injection via document content, filenames or search queries
- Secrets disclosed through logs, traces or error responses
Out of scope: vulnerabilities in third-party model weights or dependencies (report those upstream), and denial of service from deliberately oversized inputs on an instance you control.
-
No default credentials. Every credential is required configuration; the deployment manifests fail to start rather than falling back to a default. If you find a code path that accepts a built-in default, treat it as a bug and report it.
-
A session cannot reconfigure the server. The settings a session may set for itself (REST config routes, the MCP config tools, the configurator, a config import) are retrieval and generation settings. The vector backend, its connection settings, the collection, paths, URLs, credentials,
*_trust_remote_codeand server limits are refused there, with the key named; they come from the environment and the settings file only. -
The API is unauthenticated by default and intended to run on a trusted network.
X-Session-IDis accepted as identity with no credential, so anything that can reach the port can read or delete any session's documents.Set
RAG_REQUIRE_AUTH=trueto change that. Identity then has to come from a token issued byPOST /api/v1/sessions, and asession_idmerely asserted in a header or query parameter is refused with 401.RAG_JWT_SECRETmust be set to the same value in every replica: it is generated per process otherwise, so a multi-replica deployment rejects roughly half of its own tokens. The app refuses to start rather than doing that.With auth on, minting a token needs an API key: send
X-API-Keywith one of the comma-separated keys inRAG_API_KEYS(each at least 32 characters; placeholders are ignored). With no usable key configured the endpoint returns 503, so an exposed instance cannot be left open by omission. The key's principal (a digest of the key, never the key) is written into the token, and rate limits and quotas are counted per principal, so minting more sessions buys no more requests and no more storage: a key's sessions share one quota. A key mints at mostRAG_MAX_SESSIONS_PER_KEY(100) sessions per token lifetime; one more answers 429. Removing a key fromRAG_API_KEYSends every session it minted: the principal is checked against the configured keys whenever a token is read, on REST and MCP. Tokens minted withRAG_ADMIN_TOKENname no key (their principal isadmin); changingRAG_JWT_SECRETends those, and every other token. A token minted while auth was off names no principal at all, and is refused once auth is on, so switching auth on ends those sessions without a new secret.RAG_ADMIN_TOKENis a narrower thing and is not a password for the API. It guards the operations whose blast radius is the whole instance rather than one session:POST /api/v1/admin/gc/run,GET /api/v1/admin/gc/orphans,POST /api/v1/admin/gc/repair,GETandPATCH /api/v1/admin/gc/policy,DELETE /api/v1/admin/sessions/{id}, minting a token that names someone else, and the five MCP tools that mirror those. Leaving it unset does not open those routes; it makes them return 503. -
Where the bind check does and does not run.
lasagna serverefuses to start on a non-loopback interface withRAG_REQUIRE_AUTHunset, because the bind address is an argument it can read. Servinglasagna.api.rest.app:appwith your own gunicorn or uvicorn (which is what the bundledDockerfiledoes) bypasses that check: inside a container the app always sees itself bound to0.0.0.0regardless of how the port is published, so it cannot tell an exposed deployment from a loopback one. The shipped k8s manifests therefore setRAG_REQUIRE_AUTH=truein the ConfigMap, anddocker-compose.ymlpublishes the API on127.0.0.1by default. -
Cross-origin requests are denied by default. Name the origins you serve in
RAG_CORS_ORIGINS. A wildcard combined withRAG_REQUIRE_AUTHis a startup error: this API authenticates with a header rather than a cookie, so a wildcard would let any page a user visits drive authenticated requests from their browser.CORS is not a CSRF defense, and it is worth being precise about why. It governs whether the browser hands the response back to the calling page; it does not stop the request executing. Requests that require a preflight are blocked (
DELETE /api/v1/documents/{id}among them), but a "simple" request needs no preflight, andPOST /api/v1/documents(multipart/form-data) andPOST /api/v1/documents/{id}/archive(no body) both qualify. On an instance running withoutRAG_REQUIRE_AUTH, a page the operator visits can therefore still write to it blind.RAG_REQUIRE_AUTHis what closes this: identity then has to arrive inX-Session-Token, a custom header that forces a preflight, and?session_id=stops being accepted, including the anonymousdefaultnamespace. This is one more reason the unauthenticated default is only defensible on loopback. -
Forwarding headers are trusted only from a proxy you name. By default
lasagna serveignoresX-Forwarded-For, so the client address used for rate limits is the connecting socket's. Behind a reverse proxy, setRAG_FORWARDED_ALLOW_IPSto the proxy's address (comma-separated for several). Trusting every local address instead let any client on the host choose its own address, and with it a fresh rate-limit bucket per request. When you run the app under your own gunicorn or uvicorn, their--forwarded-allow-ipssetting governs this instead. -
With auth off, the HTTP surfaces accept only loopback
Hostnames. The REST API, the embedded/mcpandlasagna-mcp --transport sse(on any bind address) answer a request naming any other host with 421. That blocks DNS rebinding, where a web page points its own host name at 127.0.0.1. Inside a container network, where clients use a service name, or behind a reverse proxy that forwards its own name, add it withRAG_ALLOWED_HOSTS(comma-separatedhostorhost:*entries); compose setsapi:*,mcp:*. The health and/metricspaths answer anyHost, for probes and scrapes by pod address. Origins inRAG_CORS_ORIGINSstay allowed. With auth on, theHostis not checked: every request needs a token. -
MCP over stdio is deliberately not authenticated. The client launched the process and owns its pipes, so the process boundary is the credential. Network transports (
--transport sse, or the/mcpmount inside the REST app) are held to the same rules as REST. On any network transport, with or without auth,upload_documentreads server-side paths only insideRAG_MCP_UPLOAD_ROOT; unset, it reads none. -
The MCP server serves no deletion tools by default. The
safetool set leaves out the six tools that delete data or reset configuration;--tools allorLASAGNA_MCP_TOOLS=allserves them. -
Request bodies are limited before they are read. An upload over
RAG_MAX_FILE_SIZE, a batch overRAG_MAX_BATCH_BYTESand any other body over 1 MiB is refused with 413 from itsContent-Length, or as soon as a chunked body passes the limit. A batch of more thanRAG_MAX_BATCH_FILESfiles is refused with 400. The defaults are in the deployment guide. -
Model weights are third-party code downloaded at runtime and executed in your process. Pin versions and use a private mirror for sensitive deployments.
-
Installed plugins execute on import. Lasagna discovers components and benchmarks through the
lasagna_componentsandlasagna_benchmarksentry point groups, and loading an entry point runs that package's module-level code. This is how Python packaging works rather than anything specific to Lasagna, but it is worth stating plainly because third-party plugins are invited: anything installed in your environment can run when the registry loads. Treat a Lasagna plugin with the same care as any other dependency, and prefer a locked environment for anything sensitive.lasagna plugins listshows exactly which third-party entry points are present and which failed to load. Discovery failures are deliberately non-fatal so one broken plugin cannot stop the registry; setLASAGNA_STRICT_PLUGINS=1to raise instead of continuing.