Security assessment¶
Last performed: 2026-09-01, against the 1.0.0 release candidate. Re-run this assessment for any release that adds a new transport, provider, credential flow, or parser.
This page records what could realistically go wrong when you use lm15, what the library does about it, and what it deliberately leaves to you. The vulnerability reporting process lives in SECURITY.md.
Trust model¶
- You choose the endpoints. lm15 sends requests to the base URLs you (or the built-in provider defaults) configure. It never follows redirects and never rewrites a host behind your back.
- Providers are semi-trusted. We trust them with the request content
you send, but we parse their responses defensively: a malicious or
compromised server should produce a clean
ProtocolErrororTransportError, not memory exhaustion or code execution. - Your machine is trusted. Credentials read from environment variables or local credential files are assumed to be yours. lm15 protects them from accidental disclosure (logs, reprs, world-readable files), not from an attacker who already controls your machine.
What an attacker might try, and what stands in the way¶
Steal API keys or OAuth tokens¶
- All provider
api_keyfields are declaredrepr=False; the OAuth adapters (Claude Code, Codex) override__repr__explicitly. Printing or logging a provider object does not disclose secrets. lm15.doctorexplains credential resolution without ever returning secret material, by construction.- Refreshed OAuth credentials are written atomically with
0o600permissions (owner-only) under a lock file (lm15/_authlock.py). - OAuth login flows use PKCE (S256, 64 random bytes from
secrets), a callback listener bound to127.0.0.1only, andstatechecking. Token refresh goes to hardcoded HTTPS endpoints. - Known trade-off: the Gemini Live websocket authenticates with
?key=in the URL, as the API requires. The connection iswss://(encrypted), so the exposure is client-side URL logging only. If Google's live API gains handshake-header auth, we should switch.
Intercept traffic (adversary in the middle)¶
- TLS certificate verification is on by default, using the system
trust store via
ssl.create_default_context(). verify=Falseexists for local development against self-signed servers. It is never a default and must be typed explicitly. Preferca_bundle=for private test CAs.- Proxies: only plain-HTTP proxies are supported; HTTPS targets are tunneled with CONNECT and the TLS handshake runs end-to-end to the origin — the proxy never sees inside the tunnel.
Smuggle or inject through the HTTP layer¶
The stdlib-only HTTP/1.1 codec (lm15/transports/_http11.py) is the
most exposed code and is written accordingly:
- CR/LF/NUL bytes are rejected in the method, target, host, and every header name and value (CRLF-injection screen).
- Response heads are capped at 64 KB; chunk-size lines at 16 KB.
Transfer-Encodingstacking (gzip, chunked) is rejected; malformed or negativeContent-Lengthis rejected; data past the declared length is a protocol error. These are the classic request-smuggling and desync levers.- SSE streams enforce a 64 KB per-line and 1 MB per-event limit.
- Connect, read, and write timeouts all have non-infinite defaults.
Trick the library into running code¶
- The codebase contains no
eval,exec,pickle, subprocess, or shell invocation. All parsing of provider data isjson+ hand-written byte parsers. - There is no automatic tool-execution loop: when a model requests a tool call, lm15 hands the parsed request to your code and does nothing on its own. Executing model-chosen actions is always an explicit decision of the application.
Poison the supply chain¶
- Runtime dependencies: none (Python stdlib only). The single
optional extra is
websockets, needed only for live sessions. - Behavior is pinned by the cross-language conformance corpus in
lm15-contractat an exact SHA (CONTRACT_PIN); CI fails on drift.
Accepted risks (known and deliberate)¶
- Unbounded non-streaming response bodies. A hostile server could
send an enormous body to a non-streaming call and exhaust memory.
You choose your endpoints, so we accept this; a
max_bodycap is a candidate hardening if untrusted-endpoint use cases appear. verify=Falseexists. It is an explicit, documented, local-development-only escape hatch.- Gemini Live key-in-URL (see above) — required by the API today.
Re-assessment triggers¶
Run this assessment again (and update this page) when any of these change: a new transport or protocol, a new credential flow, a new parser over untrusted bytes, redirect support, or a new dependency.