External security review pack
Scope, trust boundaries, the evidence the project produces and the review log for an independent reviewer.
Everything an independent reviewer needs to assess the OpenSmartRoute control plane against the threat model in Security model: scope, trust boundaries, the evidence the project already produces, how to reproduce it, the questions we want answered and how a report is published. It exists so that an independent review can be run by a third party without a briefing call.
1. Engagement
| Subject | opensmartroute 2.1.0 (pip install opensmartroute), the osr serve image, the hosted platform at https://api.opensmartroute.ai / https://opensmartroute.ai |
| Type | White-box: full source, tests, CI and deployment code are shared with the reviewer for the engagement (the source repository is private) |
| Rules of engagement | Test against your own osr serve / python -m osr_platform.app instance or a free-plan workspace you created; no load testing of the hosted platform; no access to other workspaces' data |
| Contact | security@opensmartroute.ai (Reporting vulnerabilities); acknowledgement within 72 h |
| Publication | Findings fixed or accepted first; then the report is linked in section 7 and in the roadmap |
2. System under review
caller ── HTTPS ──> platform/web (Next.js, proxies /api,/v1,/mcp) ──> platform/api (FastAPI, osr_platform)
│ API keys, sessions, plans, metering,
│ workspaces, audit, admin
└──> opensmartroute (SDK: signals,
policy, strategies, learners)
└──> targets (Azure OpenAI,
MCP tools, agents, humans)
self-hosted: caller ──> osr serve (src/opensmartroute/server.py) ──> opensmartroute ──> targets
library: your code ──> Router / RouterBuilder ──> targetsTrust boundaries, from the outside in:
- Internet -> web / API. Anonymous: landing, docs,
/api/v1/models,/rankings,/stats/public,/status,/estimate(rate limited),/signup(5 per IP per day). Everything else needs an API key (osr_live_, hashed at rest) or a browser session (osr_sess_, 30 days, SSO via PKCE with a signed state). Admin routes needX-Admin-Token. Cross-workspace reads are 404 by construction (db.usage_by_request(account, request_id)scopes every trace / event lookup). osr serve. Optional bearer /X-API-Keymiddleware (serve --token,--require-auth,OSR_SERVER_AUTH_TOKENS);OPEN_PATHSare the probes,/metrics,/whoamiand the OpenAPI UI. The Helm chart and the Docker image are documented as "front it with your gateway".- Router -> request text.
InputGuard(size, gadget and steering scrub, optional PII redaction) runs before signals;Policyruns before scoring and cannot be outweighed; the LLM judge sees a fenced, sanitised prompt and its output is parsed item by item. - Router -> catalogue. Imported MCP tool descriptions, A2A cards and
SKILL.mdfiles pass the injection lexicon and the learned gadget score; MCP manifests can be signed (HMAC-SHA256 / Ed25519). - Router -> execution.
OriginPolicy(parameter provenance),ResourceLimiter(steps, calls, depth, tokens, cost, wall-clock), breakers and budgets sit between a decision and a handler call;OutputGuard(secrets, PII, relayed injection - flag / redact / block) sits between the handler's answer and the caller. - Router -> state. JSON only, atomic writes, hashed keys, versioned schema, quarantine of corrupt
state, optional AES-256-GCM at rest, hash-chained audit with
verify(). - Platform -> workspace. API keys carry scopes (
route,execute,teach,manage), an expiry and per-key limits (daily / monthly budget, allowed targets, requests per minute); the workspace policy and tenant budgets apply on top;GET /api/v1/governance/reportrenders the controls in force, the key posture and the guard verdicts of the last 24 h as the evidence pack a reviewer would otherwise assemble.
3. Threat model
SECURITY.md, "Threat model & controls" lists T1-T19 with the OWASP Top 10 / OWASP LLM Top 10 mapping and the implementing code; "Threat-model delta: 0.3 -> 0.4" lists the residual risk we already know about per threat. The reviewer is asked to confirm, refute or extend that table rather than start from a blank page.
4. Evidence the project produces
| Evidence | Where | How to reproduce |
|---|---|---|
Static analysis on every push: ruff (incl. the S bandit rules), bandit, mypy | CI, every push | python -m ruff check src tests scripts; python -m bandit -q -r src -c pyproject.toml; python -m mypy src (in the engagement checkout) |
Weekly security scan: bandit over SDK and platform, pip-audit, npm audit, safety suite | the weekly security run | shared with the engagement |
| Unit tests for the controls (guard, output guard, moderation, gadget detector, sanitiser, provenance, limits, signed manifests, audit chain, encrypted store) | the security test suites (49 dedicated tests plus hardening and store suites) | python -m pytest tests/test_security.py tests/test_hardening.py -q (in the engagement checkout) |
| Safety-routing red-team suite (paraphrase, gadget and steering attacks against safety routing) gates CI and every release | security.safety.run_safety_suite | osr -t examples/targets.yaml safety --learned-guard |
| Platform authorisation tests (API keys, scopes and per-key limits, sessions, workspaces, SSO state, device flow, operator MFA, admin token, trace scoping, governance report) | the platform test suite (419 tests) | python -m pytest platform/api/tests -q (in the engagement checkout) |
| Zero runtime dependencies in the SDK core; optional extras imported lazily | pyproject.toml | pip install opensmartroute && pip show opensmartroute (Requires: none) |
Reproducible releases: the sdist and wheel are built once by the release process, published with SHA256SUMS and uploaded to PyPI from that artifact | the release verification stage | sha256sum -c SHA256SUMS (from the downloads page) against the PyPI files |
| Container hardening: non-root image, read-only root filesystem, dropped capabilities, NetworkPolicy, PDB in the Helm chart | the published image and Helm chart | helm template over the published chart |
| Content-free logs and traces (ids, digests and lengths only) | observability.text_digest, LoggingTelemetry | run osr serve with a FileSink and inspect |
| Hash-chained audit log with tamper detection | enterprise.FileAuditSink.verify | edit one line of an audit file and run verify() |
5. Questions for the reviewer
Ordered by the impact a positive finding would have.
- Rerouting (T1, T2). Can a request author steer the decision to a chosen target through the text
alone - gadgets the learned detector misses, steering the scrub does not remove, or judge-prompt
escapes past the sanitiser? Success = a reproducible prompt that moves a target's propensity by more
than the ensemble's stated confidence margin on
examples/targets.yaml. - Policy bypass (T3). Does any strategy, plan slot, fallback, cascade or
exclude/candidatespath reach a target thatPolicy.checkrejected (PII to apii_allowed: falsetarget, region or tenant constraint)? The platform's/api/v1/policyoverlay is in scope. - Cross-workspace access (platform). Any read or write that crosses
account_id: traces, events, activity, savings, API keys, invites, SSO connections, marketplace listings, admin routes without the admin token, IDOR onrequest_id. - Authentication (platform). API-key hashing and comparison, session fixation / expiry, SSO PKCE
and state signature, the RFC 8628 device flow (
slow_down, code entropy, approval binding), signup rate limiting, the OpenAI-compatible/v1proxy's key handling. - Catalogue poisoning (T13, T14). An MCP tool description, A2A card or
SKILL.mdthat passes the import filter and changes routing or smuggles a parameter pastOriginPolicy. - Learner poisoning (T5). Feedback (
/feedback,Outcome) sequences that move a target's learned quality faster than the documented prior / confidence ramp allows, or state files that load and are trusted when they should be quarantined. - Resource amplification (T6, T15). Inputs or plans that escape
InputGuardlimits,ResourceLimitercaps or the knapsack meters; unbounded growth in any in-memory map or state store. - Data at rest and in logs (T4, T16). Any code path that writes request text to logs, traces, cache
keys, feedback, audit or metrics; weaknesses in
EncryptedStateStore(nonce reuse, key rotation). - Supply chain (T11). Release workflow, attestation coverage, Docker base image, the
install.sh/install.ps1bootstrap scripts served from the web app, the npm package@opensmartroute/sdk. - Answer-side leakage (T19). A model answer that carries a credential, personal data or an instruction
past
OutputGuardinredact/blockmode - credential shapes outsideSECRET_PATTERNS, PII entities theRedactormisses, injection phrasing underinjection_threshold, or the streamed path where the guard can only flag after the fact.
6. Known gaps (do not re-report)
- The SDK does not authenticate callers;
osr serveis unauthenticated unless--require-author tokens are configured, and the Helm chart says so. - The gadget detector ships trained on synthetic gadgets; the injection lexicon is English-centric.
- A compromised LLM judge can still bias ranks among legitimate candidate ids.
- A valid-looking poisoned learner snapshot is indistinguishable from real learning; write access to the state store is the boundary.
- Audit verification is only meaningful if the log file is write-once at the OS level.
- Encryption at rest is opt-in; key management is the operator's (
load_secret). - The output guard under
redact/blocknever lets an unscreened token out of a stream: by default it buffers the streamed answer (buffer_streams, the caller sees the screened text chunked after the fact); withbuffer_streams=FalseaStreamScreenholds back the newestOSR_SERVER_OUTPUT_GUARD_HOLD_CHARScharacters and releases only cleared text, so a secret split across chunks is caught - the trade-off is that an injection phrase longer than the hold may have partly left beforeblockstops the stream.flaginspects a stream after the fact. The hosted platform defaults toflag(OSR_PLATFORM_OUTPUT_GUARD),osr servetooff.
7. Review log
| Date | Reviewer | Scope | Report | Findings (open / fixed) |
|---|---|---|---|---|
| 2026-10-09 | internal (privacy data-flow and retention) | section 8 of this page: every store of personal data on the hosted platform, its processor and its retention, checked against the code | this page | 2 open / 2 fixed (listed under 8.4) |
| 2026-10-14 | internal (registration abuse, black-box against production) | the self-serve door after a day of scripted registrations (nine accounts, one random *.cc.cd subdomain each, Hermes <hex> names, some confirmed their address, IPs rotated under the 5-a-day cap); security headers of the API and the web app; enumeration on sign-up, password reset and login | this page | 1 open / 3 fixed (listed under 9) |
A completed review adds a row here, a "Threat-model delta" row in Security model for anything that changed, and a CHANGELOG entry for every fix. Until a row exists, the independent report is an open adoption item, not a shipped control.
8. Privacy: data flow and retention (hosted platform)
Reviewed 2026-10-09 against the code; the retention figures below are the defaults a fresh deployment runs with and a test holds this table to the constants, so a change in either shows up as a red build. The customer-facing summary is the privacy policy; PLATFORM.md, "Privacy and data handling" has the request-path diagram.
8.1 Where personal data goes
| Data | Written by | Leaves the platform to | Purpose | Store |
|---|---|---|---|---|
| Request text (prompt, messages, tool arguments) | /api/v1/route, /v1/*, /mcp | the model provider the router chose (the workspace's own key, or the deployment's); PII masked first unless the target is marked pii_allowed | answer the request | not stored unless the workspace sets log_payloads (redacted masks PII and credential shapes; full keeps the text) -> request_logs |
| Decision rows: target, cost, tokens, latency, signals, outcome | every request | nowhere | usage, billing, learning, savings | usage, feedback |
Conversations and remembered facts (osr.conversation, memory) | the chat proxy when asked | the provider, as the context of the next turn | continue a thread, personalise | conversations, conversation_turns, memories |
| Spans and events (ids, digests, lengths - no text) | every request | nowhere (an optional OTLP exporter when the operator configures one) | traces, health | telemetry_events |
| Account: e-mail, name, password hash, identities, sessions, MFA secret | signup, sign-in | the mail provider (Microsoft Graph) for verification, notices and newsletters; the identity provider on SSO; Stripe (e-mail, name, workspace id) when a plan is bought | identity, billing | users, sessions, identities |
| Sign-in place (IP, resolved country / city) | sign-in, key use, operator sign-in | a GeoIP service the operator configures (IP only) | security alerts ("new country") | sessions, audit_log, geo_cache |
| Browser and client events (extension / desktop / IDE: counts, hosts, no text) | the apps, when the person opted in | nowhere | the person's own usage page | client_events, observed_usage |
| Visitor journey (page, referrer, UTM, anonymous id) | the public site | Google Analytics 4 and the advertising conversion endpoints only when the operator configured them and the visitor consented | the activation funnel | journey_events, journey_answers |
| Uploaded bills and usage exports | bill analysis | nowhere (parsed in process) | the AI-spend report | spend_imports |
| Support tickets, sales inquiries, product feedback (incl. screenshots) | the forms | the mail provider (acknowledgements), the operators' inbox | support, sales | their own tables |
| Audit records (actor, action, subject - no text) | every privileged action | nowhere | accountability | audit_log (SQL) and the hash-chained JSONL files |
8.2 Retention
Deletion runs in the retention sweep (every 6 hours, once at start); a workspace on Team or Enterprise can set its own shorter request-row retention, which wins over the deployment default.
| Store | Kept for | Set by |
|---|---|---|
usage, feedback, request_logs (request rows) | 365 days | OSR_PLATFORM_RETENTION_DAYS (0 keeps forever) |
telemetry_events (spans, events) | 14 days | OSR_PLATFORM_TELEMETRY_RETENTION_DAYS |
observed_usage (chat-site usage the extension observed) | 90 days | fixed |
client_events (extension / desktop activity) | 7 / 30 / 90 / 365 days by plan (Free / Pro and Pay-as-you-go / Team / Enterprise) | the plan's activity_days |
journey_events, journey_answers | 400 days | fixed |
status_samples (raw uptime samples) | 7 days | fixed; daily roll-ups keep OSR_PLATFORM_STATUS_HISTORY_DAYS + 30 |
| mail outbox | 30 days | fixed |
IP address and place on audit_log, sessions, operator_sessions | 365 days, then scrubbed to null (the event row stays) | OSR_PLATFORM_NETWORK_DATA_RETENTION_DAYS |
geo_cache (IP -> place) | 30 days valid, purged after 60 days | fixed |
| newsletter engagement | 180 days | fixed |
| unverified accounts | 14 days | OSR_PLATFORM_UNVERIFIED_ACCOUNT_TTL_DAYS |
| conversations, memories | until the person or the workspace deletes them (per-thread cap of 400 turns, per-subject memory cap) | - |
audit_log (SQL) | at least 365 days | the retention default, floored at a year |
8.3 Rights
GET /api/v1/me/export returns the user's profile, identities, sessions and history and the workspace's plan,
members, key prefixes, tenants, policy, SSO, usage, requests, outcomes, request logs, evaluation reports and -
since this review - conversations with their turns and remembered facts. POST /api/v1/me/delete removes the
user (sessions, identities, memberships; workspaces they alone owned are disabled with their keys);
POST /api/v1/workspace/delete removes an organization and everything scoped to it, memory included;
DELETE /api/v1/activity/logs erases kept payloads on their own.
8.4 Findings of this review
- Fixed.
log_payloads: redactedmasked PII but not credentials: a pasted API key reached the log row verbatim. Redacted mode now runs the output guard's secret patterns as well. - Fixed. The export did not carry conversations or memories although the privacy policy promises the
dashboard removes them; it does now (
workspace.memory). - Open. Conversations have no age-based retention; a workspace that never deletes keeps them until it is deleted. The per-thread and per-subject caps bound the volume, not the age.
- Open. Journey events are unlinked, not deleted, when a workspace is deleted (the anonymous visitor rows stay for their 400 days); the privacy policy says so, a reviewer may want it tightened.
9. Findings of the 2026-10-14 registration review
- Fixed. A domain rule cannot keep up with a bot that mints a subdomain per account. The signup guard now scores
the shape of the registration (digits woven into the local part, a letters-and-digits subdomain label, a word-and-hex
display name, a long consonant run) and refuses what adds up to a machine (
block_generated, on by default); the per-domain daily cap counts by the registered domain;cc.cdand the tempmail.plus family join the temporary-inbox list. Evidence:platform/api/tests/test_signup_guard.pypins the nine observed registrations as refused and a list of ordinary addresses as accepted. - Fixed. Clearing the accounts already made meant one rule and one disable per row. The Users page's bulk actions Block their e-mail domains / Block their addresses write the blocklist rule and disable the user in one move, audited per user.
- Fixed. The bot challenge (Turnstile / hCaptcha) was configurable on the API but the web app's Content Security
Policy would have blocked its script and iframe in production; both providers are now in
script-src,frame-srcandconnect-src. - Open (accepted).
POST /api/v1/signupanswers409for an address that already has an account, which lets a caller test whether an address is registered. Password reset (202either way) and sign-in do not. The409is kept because the form shows the API key on the spot and sends the person to sign in otherwise; the per-address sign-up limit (OSR_PLATFORM_SIGNUP_PER_IP_PER_DAY, 5 by default) and the IP blocklist bound the rate at which a list can be tested. A deployment that must not disclose membership runs withOSR_PLATFORM_SIGNUP=falseand invitations. - Operator action, not code. The hosted deployment runs without a bot challenge; set
OSR_PLATFORM_CAPTCHA_PROVIDER=turnstilewith the site key and secret, a per-domain cap andauto_block_afteron the Access page.
Something wrong or out of date on this page? Tell us - the page is named for you, so a one-line note is enough.