Rullst threat models
Model version: TM-13.0 Applies to: the v13 development source and generated applications; the frozen v12 branch (end of life since 16 October 2026) keeps TM-12.10 Last source review: 2026-09-03 Status: maintainer baseline; application owners must extend it for their data, topology and providers. It is not a pentest or certification.
These models turn security ambitions into named abuse cases. A control is not effective merely because its type exists: it must be mounted in the deployed application and its negative case must pass. The security architecture defines the canonical HTTP boundary and the v12 release audit records repository evidence.
Method and common boundaries
The method is a lightweight STRIDE review: spoofing, tampering, repudiation, information disclosure, denial of service and elevation of privilege. Every model distinguishes untrusted network input, authenticated application identity, process-local state, shared durable state and third-party/hardware trust.
Process-local replay caches, counters, rate limits and audit buffers are useful
single-instance controls. They are not distributed guarantees. Forwarded
addresses are untrusted unless an explicit proxy policy (Core’s
TrustedProxyLayer, see CORE-03) establishes the direct peer as trusted.
The machine-readable release minimum in
.github/threat-model-release-minimum.json binds 52 distinct abuse-case IDs to
70 evidence rows and 69 exact test executions across fourteen packages
(thirteen crates and the LMS example, rullst-lms-example). The gate
rejects missing markers, missing tests and zero-test filters before executing
Core (including trusted-proxy client resolution), ORM, Auth, Connect, Security,
Messaging, Nexus, Studio, the production monitor, tenant ownership, Capital, AI, Mail, IoT,
generated-default and Academy negatives. Passing that bounded minimum does not
imply that every case below is closed.
TM-CORE-1 — readiness and graceful request drain
Assets: service availability, readiness state, accepted requests and dependency-health observations.
Trust boundaries: orchestrator ↔ health endpoint; application dependency checks ↔ process-local readiness bits; new requests ↔ draining server.
| Abuse case | Required disposition | Repository evidence or remaining work |
|---|---|---|
CORE-01 forged readiness or diagnostic disclosure | Readiness must fail during startup, any required-component failure, corrupted component state and drain; keep liveness process-only and return counts rather than component labels or error details. | The lifecycle-aware HTTP regression crosses startup → ready → draining, proves 503/200 transitions and verifies that the private component label is absent. Applications still own bounded dependency probes and determine which failures should gate traffic. |
CORE-02 shutdown race admits new work or abandons accepted work | Change admission monotonically to draining before the server’s graceful wait, reject later requests and bound the operator’s wait for already accepted requests. | The concurrency regression holds one admitted request, begins drain, rejects a second request with 503, observes a typed timeout, releases the first and reaches zero. A real ephemeral-listener test proves the Server ready → shutdown → stopped lifecycle. The v13 candidate additionally retains admission through response data/trailers and proves streaming drain with two real processes and an existing proxy; hosted admission is pending. Load-balancer propagation, client retry/idempotency, supervisor kill deadlines and multi-replica coordination remain deployment/application work. |
Release-negative minimum: unready/draining response without private labels; request accepted before drain completes while a later request is denied.
TM-CORE-2 — client identity behind reverse proxies
Assets: rate-limit, lockout, ban and audit identity; the TLS evidence used by Nexus Basic Auth; availability of clients that share a proxy.
Trust boundaries: arbitrary client ↔ forwarding headers; socket peer ↔ configured proxy networks; trusted proxy ↔ its appended entries.
| Abuse case | Required disposition | Repository evidence or remaining work |
|---|---|---|
CORE-03 forged forwarding metadata chooses or collapses the client identity | Read X-Forwarded-For or RFC 7239 Forwarded (exactly one) only from a socket peer inside a bounded, validated list of trusted networks; never trust /0 or an empty policy. Walk right to left, skip trusted hops, bound examined bytes/hops and keep the socket peer without failing the request when the trusted part is missing, malformed, unidentified or over-long. Report the forwarded scheme only after explicit opt-in. | TrustedProxyLayer unit tests cover direct-client spoofing, multi-hop chains, IPv6/brackets/ports/IPv4-mapped forms, unknown/obfuscated nodes, quoted-comma injection, byte/hop limits and client padding that is never parsed. Router tests prove Core RateLimiter, Security rate_limit_middleware and Nexus Basic Auth lockout bucket each client behind a trusted proxy while forged headers from a direct client are ignored, that only a trusted peer’s https report satisfies Nexus TLS evidence, and that Server mounts the layer outside its rate limiter (static and hot-reload paths). A trusted proxy that forwards client-supplied headers unchanged, proxy address spoofing on a shared network, CDN/provider range maintenance and distributed limits remain deployment work. |
Release-negative minimum: a direct client forging forwarding headers keeps its socket identity, and two clients behind one trusted proxy receive separate rate-limit buckets.
TM-AUTH-1 — sessions, passwords, OAuth/OIDC and passkeys
Assets: password verifiers, session keys/tokens, OAuth state and nonce, passkey challenges/credentials, recovery paths and account identity.
Trust boundaries: browser ↔ application; application ↔ OAuth/OIDC provider; application ↔ session/challenge store; operator ↔ key store.
| Abuse case | Required disposition | Repository evidence or remaining work |
|---|---|---|
AUTH-01 forged, truncated, expired or legacy session | Reject before constructing identity; authenticate encryption version, expiry and payload. | Negative session parsing/encryption tests in rullst-auth. |
AUTH-02 weak/missing application key | Fail startup/configuration closed outside explicit deterministic mocks. | Weak-key and malformed-key tests. Malformed .env and Rullst.toml errors carry no file content (.env names an entry number, Rullst.toml a line and column), and cookie helpers skip .env when the process selects the environment; isolated-process regressions cover each. |
AUTH-03 password timing/CPU starvation | Use Argon2id off the async executor, bound input and normalize failure behavior. | spawn_blocking implementation and password tests. The recovery registry applies the Argon2 72-byte bound before account lookup, so overlong input yields one error for registered and unknown emails; a regression covers login, registration and reset. Deployment capacity remains application work. |
AUTH-04 OAuth login CSRF/code substitution | Bind state, nonce, redirect URI, issuer, audience and one-time callback context. | The optional Axum/tower-sessions path generates a ten-minute state/PKCE challenge plus OIDC nonce, stores verifier/nonce server-side, removes and immediately saves the one active challenge before validation, and tests sequential replay, mismatch, expiry, replacement and redaction. The v13 prerelease AuthSessionForm applies the same consumption to a bounded form POST body for response_mode=form_post providers such as Apple and tests mismatch, replay, method, media-type and size rejection. The store trait does not provide distributed compare-and-delete across already-loaded requests. The v13 prerelease challenge store keeps the same challenge in the application’s database (or memory) under a digest of an opaque 256-bit id that is the only value in a short HttpOnly, SameSite=Lax cookie, and consumes it with one atomic removal: tests cover replay, expiry, wrong and missing state, missing and altered cookies, another provider’s challenge, a provider error with a forged state, and concurrent callbacks with exactly one winner (in memory and on SQLite; PostgreSQL and MySQL run against owned servers in CI). `cargo rullst make:oauth google |
AUTH-05 passkey origin/RP/challenge confusion | Require exact RP/origin, ceremony type, single-use bounded challenge, UV/UP flags and supported attestation/key formats. | Negative tests cover the bounded ES256/none scope. Challenge lifetimes are capped at one day and expiry arithmetic is checked, so configuration cannot panic and poison the challenge store. Normative WebAuthn conformance remains open. |
AUTH-06 session fixation/replay/revocation gap | Rotate on privilege change, expire server-side and provide device/session revocation. | Versioned expiring cookie sessions exist. The optional JWT policy uses bounded expiry, kid rotation, token IDs and subject session versions; production rejects its process-local store. The SQLite profile adds shared-local JTI/session-version revocation and bounded passkey inventory/revocation with counter CAS. The optional v13 shared PostgreSQL ceremony candidate adds account/session/RP-bound single use with local database/browser evidence; its monotonic shared clock lets a host up to five seconds behind adopt the recorded time and fails closed beyond that. Hosted admission remains pending. Cookie-session inventory, refresh flow, credential-repository replication/failover and application device ownership remain open. |
AUTH-07 account enumeration/recovery takeover | Normalize public responses, rate limit attempts, protect recovery factors and audit changes. | Login jail/timing helpers and subject-bound 80-bit recovery-code verifiers exist. Plaintext is returned only at enrollment and zeroized on drop; comparison is constant-time and consumption removes the verifier. Durable transactional consume, enrollment UX, step-up policy and full recovery workflow remain application-owned. |
Release-negative minimum: malformed/expired session, wrong key, cross-origin passkey, replayed challenge, wrong RP, invalid callback state and repeated login failure.
TOTP replay (AUTH-07): verify_totp_code is stateless and accepts the
previous, current and next 30-second step, so an observed code verifies again
for about 90 seconds. verify_totp_step_after accepts only a step newer than
the supplied last accepted step (RFC 6238 section 5.2); persisting the returned
step per secret atomically before admitting the login is application-owned.
TM-CONNECT-1 — durable local OAuth token generations
Assets: access and refresh tokens, provider identity, application-account binding, encryption keys and the latest accepted token generation.
Trust boundaries: application authorization ↔ account binding; provider response ↔ refresh coordinator; local processes ↔ shared SQLite state; and operator secret manager ↔ encrypted snapshot key.
| Abuse case | Required disposition | Repository evidence or remaining work |
|---|---|---|
CONNECT-24 stale or competing local writer rolls a rotated token generation back | Store only authenticated ciphertext plus non-secret key/generation metadata, serialize local writes and replace/delete only after an exact generation compare-and-swap. Reject configuration drift, quota exhaustion, malformed rows and unsafe existing file targets without exposing the database path or token material. | The opt-in SQLite store uses BEGIN IMMEDIATE, an immutable persisted row ceiling and exact successor CAS. Two independent pools race generation one; exactly one succeeds, the loser fails with GenerationConflict, restart recovers the winner, and a stale delete is rejected. Separate negatives cover key mismatch, ciphertext authentication, metadata corruption, quota/configuration, plaintext absence and symlink targets. Provider-call leases, recovery after a losing remote refresh, key-manager operation, trusted directory permissions, backup, multi-host replication and live-provider conformance remain application/deployment work. |
Release-negative minimum: two local writers cannot both replace one observed generation, and restart cannot turn a stale generation into the winner.
TM-NEXUS-1 — administrative CMS
Assets: administrative session, model records, bulk actions, AI assistant tools, audit evidence and security telemetry.
| Abuse case | Required disposition | Repository evidence or remaining work |
|---|---|---|
NEXUS-01 anonymous/default access | Fail closed without explicit production policy. Local shortcut requires debug build and verified loopback peer. | Router policy and loopback tests. Basic Auth counts only failed presented credentials per IPv4 address or IPv6 /64; credential-less challenges never lock. A locked bucket is not evaluated for unknown clients, while browsers holding the per-process known-client cookie keep access, and pruning is amortized. Behind a shared proxy a first login can still be locked out unless Core’s TrustedProxyLayer (CORE-03) is configured with the proxy’s network; a router test then proves per-client lockout isolation. |
NEXUS-02 IDOR/BOLA on CRUD/batch routes | Resolve subject/tenant, then authorize object ownership or role for every ID and bulk member. | Models that explicitly register a text tenant column now scope every built-in read/mutation/batch predicate to a trusted TenantContext, inject it on create and fail closed without context. A real SQLite HTTP regression proves cross-tenant list/update/delete/batch denial and protected create input; pure SQL tests keep the all-feature release minimum portable. #[derive(Nexus)] keeps ORM-encrypted, SecretString and #[orm(hidden)] fields hidden and read-only, shows #[orm(masked)] fields only as write-only Password widgets by default and omits skipped fields, so the panel never lists, searches or overwrites them with plaintext; derive unit and facade regressions cover this. Global models, custom routes, identity/membership resolution, within-tenant object ownership and independent review remain host work. |
NEXUS-03 stored/reflected XSS | Escape dynamic HTML; make raw HTML explicit; enforce nonce CSP. | Core proves renderer/header nonce identity. Generated LMS auth/catalog/course/player style elements consume the request nonce and remove remote shell dependencies/inline style attributes. Nexus serves its stylesheet, script and vendored htmx same-origin with no inline code, handler or style attributes; an HTTP regression checks every Nexus page and fragment against the default production CSP, and the Linux browser check loads the shell under that CSP and fails on any violation. A route-by-route review of application routes mounted next to Nexus remains open. |
NEXUS-04 AI assistant privilege escalation | Treat model output as untrusted; allowlist typed tools and authorize each invocation as the human subject. | Prompt filtering exists; tool approval/audit policy remains open. |
NEXUS-05 destructive CSRF | Require CSRF on cookie-authenticated mutations and exact signed-webhook exemptions only. | The exact Core baseline regression proves a production cookie write is denied without the matching double-submit value, accepted with it and retains the outer header/CORS policy on denial. A nested-layer regression proves an explicitly protected application router wrapped by the Server baseline emits one matching token/cookie pair and accepts the valid POST instead of applying the protocol twice. Exact signed-webhook exemptions have separate unit negatives; a full Nexus browser/proxy flow remains open. |
NEXUS-06 audit repudiation | Record actor, tenant, object, operation, outcome and correlation ID in durable separate storage. | The opt-in required policy records the built-in authenticated actor, optional tenant, table/action, optional known key, count, committed outcome, bounded correlation ID, timestamp and format version in the same transaction; unavailable storage rolls the mutation back. A real SQLite regression covers commit and rollback, while schema tests cover all SQL dialects. It is same-database mutable evidence, not separate append-only or tamper-evident storage; denied attempts and automatically assigned create keys are not uniformly persisted. Host retention, backup, replication, immutable export and review remain open. |
Trust boundaries are browser ↔ Nexus, Nexus ↔ application policy/database and Nexus ↔ LLM/provider.
TM-STUDIO-1 — local developer control room
Assets: environment/configuration, logs, traces, database browsing, job controls, prompts and source-error tooling.
| Abuse case | Required disposition | Repository evidence or remaining work |
|---|---|---|
STUDIO-01 remote exposure | Compile/mount shortcuts only in debug development and bind loopback. Server mounts the panic console, /_rullst/explain, the retired /_rullst/autofix (410 Gone, never edits files) and /_rullst/errors/{id} only when the build has debug assertions and the environment is Development, so a release binary with an unset environment does not expose them; the panic console renders details only to loopback peers, and /_rullst/errors/{id} serves the bounded, expiring, header- and body-free panic context (whose path the page’s “Ask your AI assistant” panel shows for the developer’s own assistant) only to direct loopback requests with a loopback Host and no forwarding headers. Production exposure needs application-owned auth and TLS. | Generated startup and Studio boundary tests/docs; builder console-gate, panic-console peer and error-context endpoint tests. |
STUDIO-02 secret leakage | Mask environment/log fields by key and value patterns; never render raw credentials. | Environment viewer/redactor tests cover key markers plus URL user information, bearer tokens and secret-named assignments in allowlisted values; novel formats remain residual risk. |
STUDIO-03 SQL/table injection | Parameterize values and strictly validate dynamic identifiers. | Data-browser identifier/query-builder tests. |
STUDIO-04 arbitrary source file access | Require debug loopback, canonical allowlisted paths/extensions and bounded files. | Traversal, sensitive-file, non-loopback and extension tests. |
STUDIO-05 forged telemetry | Label local/unverified events accurately; never invent source IP, HMAC verification or provider status. | Telemetry integrity tests. |
STUDIO-06 destructive job/database action | Require verified local policy and same-origin checks; remote production controls remain disabled until separately authenticated, authorized and reviewed. | Studio serves Referrer-Policy: same-origin, so browser form posts carry the real origin; Origin: null passes only with Sec-Fetch-Site: same-origin. A browser-modelled regression covers Studio’s served policy, an imposed no-referrer, cross-site and same-site (other local port) documents and a bare null origin; a local Chrome run confirmed the header values. Data-browser writes additionally require an unforgeable middleware marker, inspected complete PK, typed binds, exactly one affected row and exact delete confirmation. A key column outside the identifier boundary or column cap makes the table read-only, and the write transaction is rolled back unless exactly one row changed; a four-backend composite-key regression and a multi-row rollback test cover this. Importing the raw router is denied by an exact release-negative test. Queue retry/purge and feature-flag toggles require the same marker; raw-router negatives prove that they return 403 and leave queue records and flag values unchanged. Because safe methods pass without an Origin, GET views perform no schema or data writes; a four-backend regression proves that the feature-flag page no longer creates rullst_feature_flags. Application tenant/RBAC, durable audit and rollback remain open. |
STUDIO-07 forged or replayed remote span batch | Keep ingestion push-only; bind each endpoint to one producer name/key, authenticate the exact body and bounded source/timestamp/nonce fields, consume valid nonces atomically, reject stale/future requests and validate the complete batch before storage. | Wrong-source/key, HMAC tamper, stale timestamp, concurrent replay, schema/cardinality, deduplication and capacity regressions. TLS, key custody/rotation, clock synchronization and producer admission remain deployment work. |
STUDIO-08 cache secret exposure or broad deletion | Return metadata only, replace logical keys with keyed opaque browser tokens, omit values, omit bulk flush and require the verified-local mutation marker for one-entry invalidation. | Memory plus live Redis metadata contracts, HTML non-disclosure tests and forged/missing-marker mutation negatives. Application cache-key classification and operator policy remain external. |
STUDIO-09 database selection drift | Reuse the application’s ORM pool; otherwise resolve the database only through the shared Server/Artisan resolver, never invent a fallback database and never echo configuration content in errors. | Resolver unit tests pin the process-environment, .env and [database].url precedence, full query strings and content-free errors; an isolated-process regression proves that an unconfigured GET /studio creates no pool and no db.sqlite. A Studio request that arrives before an application’s own explicit Orm::init can still initialize the pool first; applications that choose their URL in code should initialize it before serving Studio. |
STUDIO-10 resource exhaustion through table views | Bound rows, cell text and search terms before data leaves the database or is bound per column. | Pages load 25 rows; the SELECT cuts cell text to 256 characters (key columns to 16 KiB plus one character, making longer keys read-only) and search terms above 256 bytes return 422. A four-backend regression proves that an 8 KiB cell is cut and the search bound is enforced. The per-column LIKE scan remains proportional to table size. |
Trust boundaries are local browser ↔ loopback listener, authenticated trace producer ↔ push-only application endpoint, Studio ↔ telemetry/database/cache, and error console ↔ source filesystem.
TM-STUDIO-2 — production monitoring endpoint
Added for 13.0.0-alpha.3, before the endpoint shipped. Phase 1 of the
Studio production monitor plan
is a read-only JSON endpoint, GET /_rullst/monitor/v1, that an application
mounts with Server::with_monitor. The standalone Studio that reads it for
several projects from the developer’s machine (phases 2 and 3) is modelled in
TM-STUDIO-3.
Assets: operational metrics of a production process (request counts, rates and latency per route pattern, status classes, queue depth and worker activity, possible N+1 query fingerprints, process radar); the monitor tokens; the availability of the application that serves the endpoint.
Actors: an anonymous internet client; the holder of a leaked or replayed monitor token; a user signed in to the application without the monitoring permission; a compromised developer machine that holds tokens for several projects; a browser page on another origin using a signed-in administrator’s cookies; a network observer between the client and the TLS terminator.
Trust boundaries: internet client ↔ reverse proxy/TLS terminator ↔ application listener; monitor client ↔ token or application authorizer; request handling ↔ the bounded in-process collector; collector ↔ queue store.
Test files named below are under rullst-core/src/.
| Abuse case | Required disposition | Repository evidence or remaining work |
|---|---|---|
MONITOR-01 endpoint exposed by default or by accident | Off by default. The route and the recording layer exist only when the application calls Server::with_monitor and at least one credential source is configured (a token from RULLST_MONITOR_TOKENS or code, or an application authorizer); otherwise the path answers 404 like any unknown route and request handling is unchanged. Generated starters do not enable it: the Blank, SaaS and LMS starters carry the with_monitor call and the RULLST_MONITOR_TOKENS setting commented out. | server/monitor_stack_tests.rs: no monitor and a monitor without credentials both answer 404, and an invalid token stops start-up without echoing it; monitor/endpoint_tests.rs: Installed::prepare mounts nothing (no route, no recording layer) without a credential, including an empty RULLST_MONITOR_TOKENS list. |
MONITOR-02 token guessing, theft and replay | Accept tokens only in Authorization: Bearer (never in the query string, which access logs and proxies record). Reject at startup tokens below 32 bytes of entropy (43 base64 or 64 hex characters, a minimum character variety, no placeholders) without echoing them. Compare against every configured token in constant time; several tokens allow rotation without downtime. Count only presented, invalid tokens as failures; after a bounded number per client (IPv4 address, IPv6 /64, resolved through CORE-03 trusted proxies) answer 429 without evaluating credentials until the bucket refills. Missing and invalid credentials get the same 401. A bearer token is replayable by design: TLS (MONITOR-08), rotation and the read-only scope bound the damage; there is no nonce or expiry in phase 1. | monitor/token_tests.rs (length, alphabet, variety, placeholders, list size, position-only errors, constant-time set matching, Authorization parsing); monitor/endpoint_tests.rs for missing, malformed, wrong, other-scheme, rotated and valid tokens with one identical 401, and a lockout that answers 429 even to a valid token while other clients are unaffected. Distributed (multi-replica) lockout and token expiry remain deployment work. |
MONITOR-03 signed-in user without the monitoring permission | With an application authorizer, the application decides on every request: Allow serves, SignIn answers 401, Deny answers 403, and an authorizer error fails closed with 503 and a logged reason. A token never widens what the authorizer denies for other requests. | monitor/endpoint_tests.rs: authorizer allow, sign-in, deny and error, a bearer value that is not a token, and token or authorizer either sufficing. |
MONITOR-04 information disclosure through the payload | Report only aggregates keyed by the matched route pattern (/users/{id}), never the raw path, query string, headers, cookies, client addresses, user identifiers, request or response bodies, SQL text or bound values, error messages, environment or configuration values. Requests without a matched route are counted as (unmatched). N+1 findings carry the ORM’s static Model.operation (table) fingerprint. Queue probe failures report a fixed reason, never a driver message. The schema is versioned (rullst.monitor.v1). | monitor/payload_tests.rs pins the exact set of JSON fields and values; monitor/record_tests.rs sends a path with an id, an email and a query string, a cookie, an Authorization value, a forwarded address and a user header, and finds only the route pattern in the body; monitor/config_tests.rs proves a driver error message never reaches the document. Application-chosen route templates and queue names are the application’s responsibility. |
MONITOR-05 denial of service through the endpoint or through collection | Bound collection: at most 256 route patterns (further ones share an (other) bucket), methods normalized to the standard set plus OTHER, a fixed latency histogram over a five-minute window, at most 64 N+1 entries, 16 queues and a 250 ms limit per queue probe. Collection is O(1) per request and lock-local to the route. The endpoint has its own per-client rate limit, and its requests are not counted in the statistics. The overhead per request is measured and published in the guide. | monitor/collector_tests.rs (route and method cardinality, (other), N+1 table eviction), monitor/endpoint_tests.rs (per-client budget), monitor/config_tests.rs (concurrent queue probes bounded by the timeout), monitor/record_tests.rs (own requests not counted); the release-mode overhead measurement (monitor/overhead_tests.rs) is published in the production monitoring guide. |
MONITOR-06 cache or proxy keeps a response | Every monitor response, including refusals, carries Cache-Control: no-store and X-Content-Type-Options: nosniff; staging and production also receive the security-header baseline. | monitor/endpoint_tests.rs checks the headers on 200, 401, 403, 405, 426 and 429; server/monitor_stack_tests.rs checks the production security headers. |
MONITOR-07 cross-origin read with an administrator’s ambient cookies | The endpoint is a machine contract: a request carrying an Origin header, or a Sec-Fetch-Site of cross-site or same-site, is refused with 403 before credentials are evaluated, so even an application CORS policy that allows the origin never releases monitoring data to a page. The endpoint itself adds no CORS headers and serves application/json with nosniff. | monitor/endpoint_tests.rs: Origin (including null) and cross-site or same-site Sec-Fetch-Site refused with an empty body and no Access-Control-Allow-Origin from the endpoint; server/monitor_stack_tests.rs: refused under an application CORS allowlist that includes the origin. |
MONITOR-08 downgrade to plain HTTP | In staging and production, refuse (426 Upgrade Required, before credentials are evaluated) a request without TLS evidence: an https scheme reported by a trusted proxy with trust_forwarded_proto (CORE-03), or a MonitorVerifiedTls marker inserted by an application that terminates TLS itself. A forwarded header from an untrusted peer and an absolute-form https:// request target are not evidence. An explicit opt-in admits plain HTTP from a direct loopback peer without forwarding headers (an SSH tunnel or a sidecar on the same host). A token already sent in cleartext must be rotated. | monitor/endpoint_tests.rs: staging and production plain HTTP and an absolute https:// target refused; forged X-Forwarded-Proto from an untrusted peer refused; trusted proxy https admitted and http refused; marker admitted; the loopback opt-in admits direct loopback peers and refuses forwarding headers, a public Host and other peers; development and test admit plain HTTP. |
MONITOR-09 compromised developer machine or monitoring client | A token grants read-only access to one application’s aggregates and nothing else: no rows, no secrets, no mutation, no Studio development pages. Use one token per project and client, so revoking one machine is removing its token. | The read-only scope follows from MONITOR-04; monitor/endpoint_tests.rs proves other methods answer 405. Tokens stored on a developer machine are covered by REMOTE-03 (TM-STUDIO-3). |
MONITOR-10 monitoring changes or breaks request handling | The recording layer never alters a response; it observes method, matched route, status and duration, tolerates poisoned locks and adds no work when the monitor is disabled. ORM attribution uses the passive query layer the telemetry subscriber installs; with a RUST_LOG that filters rullst_orm spans or an application-owned subscriber, the payload says so instead of reporting zero queries. | monitor/record_tests.rs compares responses with and without the layer and counts ORM operations only inside a recorded request; monitor/payload_tests.rs covers the unavailable reasons; the full Core suite passes with the passive layer. |
Release-negative minimum: disabled → 404; missing and invalid tokens →
the same 401; production plain HTTP → 426; a raw path with an id and a
query string never appears in the payload.
TM-STUDIO-3 — Studio remote projects on the developer’s machine
Phase 2 of the
Studio production monitor plan:
the standalone rullst-studio binary runs on a developer’s machine without an
application process, keeps a list of projects (name, base URL, monitor token)
and renders each project’s rullst.monitor.v1 document (TM-STUDIO-2). It
connects outwards only; the server collects nothing new. Studio’s local pages
(data browser, .env viewer, AI and API playgrounds, migrations) are not part
of this mode.
Assets: the monitor tokens of every configured project; the developer’s local network position; the integrity of what Studio displays; the availability of the developer’s Studio process.
Actors: a malicious or compromised monitored application (or anyone able to answer for its origin); a web page open in the developer’s browser; another local user on the same machine; malware running as the developer; a network attacker between Studio and the monitored application; someone who obtains a copy of the configuration file (backup, dotfile sync, screen share).
Trust boundaries: browser ↔ Studio’s loopback listener; Studio ↔ the configuration file and its separate key file; Studio ↔ each monitored origin over the network; Studio ↔ its local history database (phase 3); Studio ↔ the developer’s webhook receiver (phase 3).
Test files named below are under rullst-studio/src/.
| Abuse case | Required disposition | Repository evidence or remaining work |
|---|---|---|
REMOTE-01 Studio reachable from other machines or pages | Bind 127.0.0.1 only; every route keeps Studio’s loopback boundary (loopback peer, loopback Host, same-origin Origin on unsafe methods, the crate-private marker), which remote mode applies in release builds too because it serves no local data. | remote/router_tests.rs: a non-loopback peer, a public Host and cross-origin or Origin-less form posts are refused; bin/rullst-studio.rs: the only option is --port (a --host argument is rejected). |
REMOTE-02 local CSRF against add/remove-project forms | Unsafe methods require a same-origin Origin (as for every Studio form) and a per-process random form token compared in constant time; GET pages change nothing. | remote/router_tests.rs: a post without Origin, with another origin, or with a missing or wrong form token answers 403 and leaves the project list unchanged; form tokens are compared whole. |
REMOTE-03 token disclosure at rest | Store the project list in the user’s configuration directory (0600 file in a 0700 directory on Unix, never through a symlink, written atomically, bounded size). Encrypt each token with AES-256-GCM under a 256-bit key kept in a separate file in the user’s data directory (0600), with the project name and base URL as associated data, so a token copied to another project or a URL edited in the file no longer decrypts and is never sent. Tokens never appear in pages, logs, errors or Debug. This protects a copied configuration file, not a compromised account: malware running as the developer can read both files. | remote/store_tests.rs: encryption round trip, the file never contains the token, swapped names and an edited URL fail to decrypt, a missing key makes tokens unreadable, 0600/0700 permissions and the readable-by-others warning on Unix, symlinked files refused, malformed and oversized files reported without contents. OS keychain integration is not provided. |
REMOTE-04 token sent to the wrong party | Accept only https base URLs, or http to a loopback host (SSH tunnels). Reject user information, query and fragment. Send the token only in Authorization: Bearer to the configured origin’s fixed monitor path; follow no redirects; validate certificates (no option to disable it); ignore proxy environment variables. | remote/store_tests.rs: http to a non-loopback host, user information, query strings, fragments and non-HTTP schemes are refused; remote/client_tests.rs: a redirect to another origin is reported and the second origin never receives the token, and the token goes as Bearer to the configured origin only. |
REMOTE-05 hostile or compromised monitored endpoint | Treat the document as untrusted: bound the response (1 MiB, 5 s total, 3 s to connect), require 200, JSON and schema: "rullst.monitor.v1", bound every list and string, and HTML-escape every remote value; an error shows a fixed status, never the remote body. | remote/client_tests.rs against a local test server (remote/server_test.rs): oversized (declared and streamed), malformed, wrong-schema, non-JSON and slow responses become fixed statuses within the limits; remote/router_tests.rs: script and markup in labels are escaped on every page and unreachable projects show a status. |
REMOTE-06 SSRF-like use of the project list | Projects are added only through the protected form; Studio requests only GET <base>/_rullst/monitor/v1 and renders only the validated schema, so a project pointing at an internal service yields at most a refusal status. At most 32 projects. | Covered by REMOTE-02, REMOTE-04 and REMOTE-05 tests. Network policy on the developer’s machine stays with the developer. |
REMOTE-07 local surfaces leaking into remote mode | The remote router mounts only the remote pages; the data browser, .env viewer, AI and API playgrounds and migration pages are absent. | remote/router_tests.rs: those paths answer 404 in remote mode while the remote pages and assets answer. |
REMOTE-08 history store disclosure or growth (phase 3) | Store only aggregates the document already exposes (reachability, request rate, 5xx rate, p95, queue pending and failed totals) per project and time, never tokens, routes or other labels, in an owner-only SQLite file in the user’s data directory. Delete samples older than the retention age and beyond a per-project row limit on every write; keep a bounded number of alerts. | remote/history_tests.rs: retention by age and by row count, alerts bounded, file permissions on Unix, only numeric columns stored. |
REMOTE-09 polling turned into load or a network scanner (phase 3) | Poll only projects the developer marked as watched, at an interval bounded to 15 s–1 h (default 60 s), one read per project per interval with the REMOTE-04/REMOTE-05 bounds, so the monitored server’s own budget (MONITOR-05) is never approached. | remote/poller_tests.rs: one read per watched project per round, unwatched projects untouched, interval clamped. |
REMOTE-10 alert spam or hostile values in alerts (phase 3) | Evaluate rules with an explicit clock; a rule fires again only after its per-project cool-down; the 5xx-rate rule needs a minimum number of requests; restarts that reset counters never fire the failure rule. Alert text is built from Studio’s own wording, the project name and numbers, never from remote labels. | remote/alerts_tests.rs with a fixed clock: thresholds, cool-down, minimum requests, counter resets, unreachable projects. |
REMOTE-11 webhook secret disclosure or misuse (phase 3) | A webhook URL is a secret: stored sealed like tokens (bound to the project), shown only as configured or not, https:// only, no user information or fragment. Delivery sends a small JSON body with the project name, rule, message and time, follows no redirect, ignores proxies, is bounded to 5 s and never retried in a loop; a failure is recorded on the alert as a fixed status. | remote/webhook_tests.rs: URL validation, sealing round trip, delivery to a local test receiver, failure and redirect statuses without following. |
TM-TENANT-1 — multi-tenant data access
Assets: membership, tenant-scoped records, billing/workspace IDs, cache keys, jobs, files, logs and exports.
| Abuse case | Required disposition | Repository evidence or remaining work |
|---|---|---|
TENANT-01 trusted client header | Never accept tenant/role solely from an arbitrary header; bind to authenticated membership or a trusted gateway. | Strict tenant guard rejects absent context; gateway policy is application-owned. |
TENANT-02 object ID crosses tenant | Query with tenant/owner predicate and authorize the returned object server-side. | UserContext carries a validated optional tenant and RbacGuard::authorize_tenant[_owner_or_role] requires an exact match that even admin cannot bypass. The materialized LMS starter test exercises its enrollment-owner boundary (ACADEMY-02); general route coverage remains open. |
TENANT-03 bulk/list/export leakage | Apply tenant predicate before pagination/count/export and validate every bulk member. | Must be proven per generated/application route. |
TENANT-04 cache/queue/storage collision | Include canonical tenant identity in keys, jobs and storage roots; validate again in workers. | Core’s TenantStorage, TenantCache, TenantRealtime and TenantPresence can only be constructed from a validated TenantContext, apply immutable tenant namespaces and have exact same-key/channel/presence-room non-interference tests. Cache keys, realtime channels and presence rooms share one tenants:<tenant>:<logical> builder whose tenant segment encodes % as %25 and : as %3A, so a logical name containing : cannot alias another tenant (identifiers without those characters keep their previous names); ("a", "b:c") versus ("a:b", "c") is tested for cache, broadcast and presence. Tenant identifiers made only of dots are rejected, and TenantStorage also refuses any tenant that is not exactly one normal path segment, with an exact tenant-. negative. On local storage it also refuses a tenant whose tenants/<tenant> directory the filesystem resolves under another name, so case-variant identifiers (Acme/acme on APFS or NTFS) and Windows trailing-dot stripping (acme.) cannot share a directory; the first tenant to create it keeps it. The cache wrapper exposes no cross-tenant flush; the realtime wrappers validate names and bound payloads. Generated ORM Redis model hashes bind the application namespace plus opaque tenant and table digests, require a typed tenant context for tenant models and reject another tenant’s handle or decoded hash; an opt-in live Redis regression proves tenant/namespace separation, while the orm:events:* channels stay un-namespaced. Application room authorization, distributed realtime/liveness, search, metrics, exports, distributed cache/failover and remote bucket policy remain open. |
TENANT-05 confused-deputy background work | Persist actor/tenant/authorization intent and revalidate sensitive execution. | Shared durable job authorization contract remains open. |
Trust boundaries are identity ↔ membership, route ID ↔ object and application ↔ database/cache/queue/storage.
TM-ORM-1 — portable document recovery
Assets: document contents, portable identifiers, application/collection scope, snapshot encryption keys and the destination collection.
Trust boundaries: application writer ↔ repository inventory; source store ↔ application-owned snapshot storage; operator key custody ↔ snapshot opener; and snapshot contents ↔ destination repository.
| Abuse case | Required disposition | Repository evidence or remaining work |
|---|---|---|
ORM-01 copied, tampered or cross-scope snapshot discloses or substitutes documents | Encrypt and authenticate the complete versioned payload with a fresh nonce, explicit rotation key ID, length-delimited application/collection binding and fixed decode limits; authenticate before JSON decoding. | The exact negative changes ciphertext, key material and application binding and proves all three fail closed. Key bytes are consumed through a zeroizing temporary, opaque snapshot/key Debug output is redacted and plaintext labels are absent from the envelope. Key generation/custody/rotation, external snapshot permissions and deletion remain operator responsibilities. |
ORM-02 partial retry overwrites conflicting destination data or silently accepts extras | Accept only an empty destination or an exact matching subset, insert without replacement, treat only an exact raced duplicate as replay and verify the final complete inventory. Never delete an extra row or claim transactionality across stores. | The exact negative proves a differing row and an extra row fail before mutation. Deterministic tests cover partial resume and idempotent replay; the live matrix exports MongoDB→SurrealDB→MongoDB and checks ordered IDs. Export performs two equal observations, but formal consistency still requires the application to quiesce writers; a failed restore can retain prior successful inserts for the documented retry path. |
Release-negative minimum: ciphertext/key/scope substitution and conflicting or extra destination state.
TM-SEC-1 — declared HTTP payload contracts
Assets: route semantics, validated request data, schema configuration and security telemetry.
| Abuse case | Required disposition | Repository evidence or remaining work |
|---|---|---|
SEC-16 malformed, ambiguous or schema-confused JSON body | Require a JSON media type on unsafe schema-bound requests, classified as the route’s JSON extractor does (any ASCII case, parameters, json subtype or +json suffix) so an accepted spelling cannot skip the checks; reject malformed, duplicate-key, oversized or deeply nested input before applying a precompiled closed schema. Never fetch attacker-selected schema references. | The bounded route policy compiles JSON Schema 2020-12 or one OpenAPI 3.1 component with local references, no network/filesystem resolver and linear-time regexes. The exact negative covers shape/additional-property confusion and external references; middleware tests cover 415/400/422 and exact body preservation. Auth, ownership, domain rules and non-JSON parameters remain separate. |
Trust boundaries are application schema configuration ↔ compiled validator and untrusted HTTP body ↔ route handler.
TM-SEC-2 — anomaly assessment and proof-of-work admission
Assets: service availability, canonical client subjects, challenge key, challenge capacity and replay state.
| Abuse case | Required disposition | Repository evidence or remaining work |
|---|---|---|
SEC-07 forged, replayed, cross-subject or resource-exhausting challenge | Authenticate the complete challenge, bind it to one canonical subject, cap difficulty/TTL/cardinality, reject invalid work, expire it and atomically consume one successful proof. Classification must remain explainable and must not become authorization by itself. | The exact negative tampers with the token, changes the subject, submits invalid work, verifies concurrently and replays/expires the challenge; only one local verifier succeeds. Aggregate collection, proxy/device identity, accessible alternatives, distributed replay state, adaptive evaluation and enforcement remain application/deployment work. |
Trust boundaries are host-supplied aggregates ↔ deterministic classifier, application subject ↔ challenge and process-local replay state ↔ distributed deployment.
TM-SEC-3 — authenticated local security-event journal
Assets: normalized security-event evidence, HMAC keys, key identifiers, record order and durable local file state.
| Abuse case | Required disposition | Repository evidence or remaining work |
|---|---|---|
SEC-33 forged, substituted or rotation-confused local event | Authenticate a domain-separated canonical frame containing sequence, key identifier, predecessor tag, payload length and exact normalized event bytes; reject absent/wrong keys, forgery, reordering and removed interior records before returning an event as HMAC-verified. | The exact negative proves payload tampering, same-identifier wrong-key substitution and absent historical-key rotation fail closed. Additional deterministic tests cover restart across active-key rotation, predecessor/sequence attacks, quotas, symlink targets, external length changes and redacted/zeroizing key storage. This is one local writer and a maximum of eight keys/4,096 records/16 MiB; trusted key/path custody, whole-tail rollback checkpoints, multi-writer coordination, compaction, retention, remote delivery and acknowledgement remain operator/application work. |
Trust boundaries are local event producer ↔ authenticated journal, key manager ↔ rotation ring and filesystem state ↔ restart verifier.
TM-PAY-1 — webhooks and billing boundaries
Assets: provider secrets, event IDs, subscription state, amount/currency, invoice identity and replay state.
v13 keeps the provider-neutral base, Stripe and the experimental InfinitePay adapter. Payout and NFS-e fiscal code left the framework, so their cases below are retired and claim no current evidence; applications that add custom gateways, payouts or fiscal integrations own those threat models.
| Abuse case | Required disposition | Repository evidence or remaining work |
|---|---|---|
PAY-01 forged webhook | Verify the provider’s exact signed bytes and algorithm cryptographically before parsing side effects. | Axum and Actix call the same verifier; Stripe signature negatives plus Actix body/event preservation prove the bounded adapters. The experimental InfinitePay adapter rejects live deliveries with UnsupportedOperation because no reviewed authentication contract exists; its mock_* verification is offline-only. Custom adapters must declare their verification mode, and the trait default fails closed. |
PAY-02 replay/stale event | Enforce timestamp window and unique event ID in durable shared state before effects. | Freshness and bounded local replay are enforced before dispatch. The opt-in SQL ledger persists provider-scoped payload/event claims across SQLite/PostgreSQL/MySQL/MariaDB processes, fails closed on capacity/storage/profile errors and has restart/contention/four-protocol live evidence. Middleware claims before dispatch and is not exactly-once delivery; stable event IDs can instead share the caller’s relational domain transaction, and a live MySQL/MariaDB/PostgreSQL case proves a caller transaction whose snapshot predates a concurrently committed claim is rejected. A custom adapter for a provider that signs only the body checks no delivery timestamp, so an exact captured body verifies again once its replay claim expires (24 hours by default, at most 30 days) or, with the process-local store, after a restart; hosts must order and deduplicate their state changes durably. |
PAY-03 amount/owner substitution | Derive product/currency/owner from authenticated server state, never arbitrary client values. | Materialized SQLx/Turso billing scaffolds bind checkout to authenticated identity and deny cross-owner subscription reuse before customer binding; route/plan/provider review remains open. |
PAY-04 duplicate/partial transition | Use a transactional idempotent state machine and reconcile with the provider. | check_and_record_event_key_with_transaction can bind one stable provider event ID to one mutation in the same supported relational database. Cross-system atomicity, provider reconciliation, automatic state-machine policy, and middleware crash recovery remain open. |
PAY-05 payout destination takeover | Retired in v13. | The Wise payout adapter and payout contracts were removed; no current evidence is claimed. Applications that add payouts own step-up authentication, destination allowlists/change delays and independent audit. |
PAY-06 false fiscal authorization | Retired in v13. | NFS-e preparation was removed from the framework (it may return as a separate product); no current evidence is claimed. Rullst receipts never represent tax authorization. |
PAY-07 forged or confused SEFIN issuance response | Retired in v13. | Removed with the NFS-e module; no current evidence is claimed. |
PAY-08 replayed or substituted direct charge | Require integer minor units, currency, authoritative provider customer/payment-method identity and a durable order idempotency key; bind the accepted response before side effects. | ChargeRequest validates and redacts the bounded inputs. Stripe forwards Idempotency-Key, confirms off-session and rejects responses with mismatched amount/currency or non-accepted status. Offline receipts carry the distinct non-success Mock status and other adapters fail unsupported. The host still owns mandate/SCA setup, identity authorization, durable key uniqueness, webhook reconciliation and entitlement state. |
PAY-09 substituted or replayed paid invoice delivery | Bind the recipient, exact minor-unit total and currency to final non-mock payment evidence; expose a stable delivery identity and require durable claiming before retryable effects. | PaidInvoice rejects Processing/Mock and any e-mail, amount or currency mismatch. The opt-in Mail bridge renders bounded HTML/PDF, runs mandatory pre-flight and retains a stable delivery key. The host still owns authenticated order construction, webhook reconciliation, atomic outbox claiming, retry policy and provider acceptance; delivery is not exactly once. |
PAY-10 duplicated or response-confused metered usage | Bind every accepted response to provider-specific customer/item, metric identity, quantity, timestamp/action and retry evidence; never infer missing provider fields from a uniform subscription ID. | Stripe Meter Events forward a bounded identifier and recheck customer/event/value/timestamp/identifier, cap response bytes, redact identities and keep mocks visibly non-live. A custom adapter without a provider event key reports ApplicationOutboxRequired so the application claims the key durably. Provider-account acceptance, configured aggregation, durable storage/retry and invoice reconciliation remain application/operational work. |
Trust boundaries are provider ↔ webhook, user ↔ checkout, application ↔ provider and application ↔ durable billing state.
TM-MAIL-1 — outbound content, suppressions and delivery telemetry
Assets: recipients, message content and attachments, provider event identities, suppression state, tenant identity and operational observations.
| Abuse case | Required disposition | Repository evidence or remaining work |
|---|---|---|
MAIL-01 executable, active or type-confused attachment leaves the process | Validate bounded metadata and recognizable signatures, reject active content and fail closed when authoritative inspection is unavailable. | The opt-in static AttachmentInspectionGuard completes inspection before the wrapped transport. Its strict local heuristic rejects executable magic, spoofed known types, active PDF/SVG, secrets and unsafe text links. The declared MIME type alone never selects the checks: it is compared case-insensitively and combined with the filename extension and content signature, so an active PDF, SVG, HTML or script file labelled text/plain or application/xml is rejected (SVG and XHTML are recognized by root element after any prologue or DOCTYPE and namespace prefix, and by namespace URI, as browsers parse them), and the strict policy rejects a declared type it does not inspect (such as text/html on invoice.txt) unless it is application/octet-stream; executable/script-host extensions are rejected under both policies. PDF #xx name escapes and compressed streams are not decoded. It is not antivirus, sandboxing, recursive archive inspection or CDR; production risk policy may require an independently operated scanner adapter. |
MAIL-02 forged, replayed or lost suppression event permits unwanted delivery | Authenticate the provider event before mutation, bind provider/event/payload exactly, persist suppression durably and check it before transport. | The opt-in SQLite store provides exact replay conflict detection, monotonic manual/bounce/complaint state, immutable quotas and shared-local restart/two-instance evidence; SuppressionGuard fails closed on suppressed recipients or unavailable state. One recipient parser reduces Name <address> forms to the bare address for the pipeline, disposable check, verified feedback and store lookup, so a display name cannot miss a suppression entry; unparsable recipients are rejected. Rullst does not authenticate provider webhooks in this API, and only already-verified events may be recorded. Replay-ID retention must cover the provider window. The optional v13 PostgreSQL store shares state across hosts behind a monotonic namespace clock that tolerates at most 300 s of backwards host skew and fails closed beyond it. Multi-host replication, webhook adapters and provider-account acceptance remain open. |
MAIL-03 message or recipient leaks through delivery telemetry | Emit only bounded low-cardinality outcomes and never recipient, subject, body, filename or provider response content. | ObservedMailDriver records provider label, outcome, elapsed time, attachment count and two booleans through a non-failing static sink. The bounded default sink is process-local; externally operated metrics/tracing export, retention and alerting remain deployment work. |
MAIL-04 oversized or adversarial message content exhausts the sending executor | Bound scanned text before inspection and keep every content scan linear in its input. | The mandatory pipeline rejects a subject over 2 KiB or an HTML/text body over 2 MiB before scanning; link extraction deduplicates through a hash set and the redactors and URL search are single forward passes. A regression test runs marker-, link- and PEM-dense bodies at the limit under a deadline. Scans still run on the calling task, so hosts should also cap request bodies. |
MAIL-05 a production misconfiguration silently selects the offline mock and loses mail | Select offline delivery only from explicit mock configuration and reject partial credentials. | SMTP selects its offline fixture only for an empty/mock_* host or a mock_* username/password; a real host without credentials is a real relay and a lone username or password is a ConfigError. REST providers keep the documented empty/mock_* credential convention, so deployments should assert delivery_mode() at startup. Without MAIL_DRIVER (process environment, then .env) or [mail] driver, the facade falls back to the metadata-only log driver only in development/test, resolved with the Server environment precedence; staging and production fail with ConfigError. A MAIL_DRIVER or [mail] driver naming a transport removed in v13 (sendgrid, postmark, mailjet, mailtrap, their sandbox values or azure-acs) fails with a ConfigError naming it instead of falling back, and a queued job then fails visibly and stays retryable. |
Trust boundaries are authenticated application state ↔ mail message, application ↔ inspection/suppression adapters, verified provider event ↔ local suppression state and transport result ↔ observation sink.
TM-MESSAGING-1 — durable messages, wire frames and correlation
Assets: message payloads and headers, idempotency state, ACK leases, consumer progress, storage keys, broker routing metadata and trace correlation.
| Abuse case | Required disposition | Repository evidence or remaining work |
|---|---|---|
MESSAGING-01 protected message content is recovered from a copied SQLite file | Offer an explicit authenticated-encryption profile, encrypt header values and payload before persistence, and fail startup on profile/key mismatch. | connect_encrypted uses AES-256-GCM with random nonces and a bounded keyring. A raw-database/restart regression proves the selected header and payload are absent while delivery round-trips. Topic, event/content type, IDs, timestamps, idempotency key, fingerprint and delivery state remain visible metadata; host key custody, permissions, backup and erasure remain required. |
MESSAGING-02 ciphertext or metadata is copied, reordered or altered | Bind namespace, topic, sequence, message ID, event/content type, timestamp and rotation key ID into AAD; reject authentication failure before claiming the message. | The exact negative swaps two valid ciphertext rows and receives StorageAuthenticationFailed; probe tamper and wrong-key tests cover startup. AES-GCM does not prevent deletion, rollback of the complete database or availability loss, so protected backup/rollback detection remains deployment work. |
MESSAGING-03 unsafe rotation silently makes retained records unreadable | Track every retained record’s non-secret key ID, require every referenced prior key at startup and open one retained record per referenced key ID to prove its bytes, use only the primary key for new writes and permit removal only after old records are purged. | The rotation regression opens old+new, writes with the new primary, rejects new-only or a prior key with the right ID but wrong bytes while an old record remains, then permits new-only after terminal purge. External secret-manager rotation, escrow, recovery rehearsal and multi-host rollout remain operator work. |
MESSAGING-04 malformed, oversized, cross-namespace or future-version wire frame enters broker state | Bound the frame before payload allocation, validate every field and canonical ordering, require exact namespace and reject unknown versions/trailing bytes. | A fixed byte digest freezes v1; exact negatives cover every truncation, wrong magic, unknown version, trailing data and namespace mismatch. The codec is only an envelope primitive, not a remote transport or provider protocol. |
MESSAGING-05 attacker-controlled trace metadata leaks baggage or creates ambiguous correlation | Propagate only strictly validated W3C version-00 traceparent and a conservative tracestate subset; exclude baggage and redact diagnostics. | Grammar, duplicate-key, zero-ID, uppercase and malformed-member negatives are exact; an in-memory delivery proves only the two allowlisted headers. Sampling, tenant authorization, exporter security and retention remain host work. |
MESSAGING-06 process stops after broker publication but before acknowledging the relational outbox | Commit domain state plus outbox row together, claim with a lease, publish the stream-scoped outbox event key as broker idempotency, then ACK the exact claim. Never call the two systems one atomic transaction. | The opt-in static OrmOutboxRelay maps one configured stream/topic, validates the claimed JSON and keeps payload/event/claim keys out of Debug. An exact crash-window regression publishes, lets the claim expire, republishes through a new claim, observes the broker’s duplicate receipt and only then ACKs; one message exists. Worker supervision, retention, tenant authorization and destination idempotency remain application work. |
Trust boundaries are publisher/consumer ↔ broker contract, process ↔ SQLite file/key manager, local envelope ↔ future remote adapter and untrusted correlation headers ↔ tracing infrastructure.
TM-AI-1 — prompts, RAG and tool execution
Assets: system prompts, tenant content, provider keys, retrieved data, tool credentials, destructive operations and audit evidence.
| Abuse case | Required disposition | Repository evidence or remaining work |
|---|---|---|
AI-01 direct/indirect prompt injection | Treat prompts/retrieval as untrusted and never equate heuristic pass with safety. | The versioned rullst-ai-guardrails-v1 offline corpus runs injection/jailbreak cases across all built-in transports; adaptive and live-model evals remain open. |
AI-02 PII/secret exfiltration | Minimize/redact before dispatch and enforce tenant-aware retrieval. | The versioned offline corpus fixes exact implemented email/card/CPF/CNPJ redactions across built-in transports; CPF/CNPJ masking requires valid check digits and does not recognize alphanumeric CNPJs. RagPipeline guards/masks every selected passage, requires trusted tenant context and rejects mismatched tags; the application retriever must still apply authoritative datastore tenant/ownership predicates, and raw provider calls remain an explicit bypass boundary. |
AI-03 unauthorized tool/arguments | Allowlist typed tools, validate schema and authorize as the initiating subject after selection. | Local registry dispatch requires exact allowlist, principal authorization, closed bounded JSON, call budget and audit; destructive/financial approvals are one-use and payload-bound. Provider-native selection loops, domain authorization, approver authentication and durable audit remain open. |
AI-04 destructive autonomous action | Require human approval and idempotency for finance, deploy, deletion or privilege change. | Not yet a general framework guarantee. |
AI-05 SSRF/egress exfiltration | Validate destinations, block private/link-local/metadata networks and cap redirects/content/time. | EgressPolicy::strict() denies all hosts until an exact allowlist is configured. The opt-in EgressFetcher resolves under deadline, validates every answer, pins them into a proxy-free client, verifies the peer, revalidates manual redirects and bounds declared/streamed bytes; deterministic negatives stop private/mixed DNS before transport. It does not automatically wrap provider/application clients, and tenant-aware destination authorization, response validation plus a successful live-origin redirect/stream contract remain open. |
AI-06 unbounded cost/availability | Enforce body/token/time/concurrency budgets, cancellation and circuit breaking. | Built-in transports have configurable request deadlines and local tools have call/payload budgets; provider-neutral cancellation, concurrency limits and circuit breaking remain open. |
AI-07 hallucinated structured result | Require schema and authoritative server-side validation; prose is never authorization. | Structured-output foundations exist. |
AI-08 cross-tenant, reordered or incompletely erased chat memory | Select storage only from trusted tenant/conversation context, commit complete exchanges atomically, reject stale writers and erase the exact key. | The reusable stores bind tenant plus validated conversation ID and enforce bounded consecutive user/assistant pairs. The SQL adapter uses transactional revision CAS; its exact SQLite negative proves isolation, a single winner, deletion with foreign keys disabled and no orphaned messages, while live matrices cover PostgreSQL/MySQL/MariaDB protocols. MySQL/MariaDB key columns use ascii_bin and every tenant-scoped statement compares the key byte-exactly; the MySQL 8.0 and MariaDB matrices prove that tenant and conversation IDs differing only by case stay separate, that a legacy case-insensitive table fails closed and that the documented migration restores separation. Authentication/ownership inside a tenant, encryption, retention deadlines and backup erasure remain host policy. |
Trust boundaries are content ↔ model context, application ↔ provider, model output ↔ tool dispatcher and retriever ↔ network/data sources.
TM-IOT-1 — OTA manifest and device lifecycle
Assets: provisioned public key, firmware image/hash, target, monotonic version, boot state and telemetry.
| Abuse case | Required disposition | Repository evidence or remaining work |
|---|---|---|
IOT-01 forged/tampered manifest | Verify Ed25519 over canonical bounded bytes with the provisioned key. | Signature/tamper tests. |
IOT-02 rollback | Require version above a persisted monotonic counter before boot. | In-process anti-rollback exists; persistent integration remains open. |
IOT-03 wrong target/image | Verify target, declared length and cryptographic hash before flashing. | Manifest checks exist; downloader/flasher is roadmap. |
IOT-04 power loss/partial flash | Use a recoverable A/B boot flow and commit counter only after verified boot. | The target bank is always opposite the running bank that platform code reports; a commit does not move the running bank, and a regression proves a second update before reboot never targets it. Hardware/bootloader integration remains open. |
IOT-05 signing-key compromise | Use offline protected signing plus rotation/revocation. | Operational/HSM program remains open; simulators are not HSMs. |
IOT-06 telemetry spoof/replay | Authenticate channel/device and bind identity plus sequence/time. | Transport identity/MQTT remains open. |
Trust boundaries are signer ↔ distribution, distribution ↔ device, verified manifest ↔ flasher/bootloader and device ↔ backend.
TM-ACADEMY-1 — education, assessment and games
Assets: school and tenant membership, learner identity, protected content, enrollment/entitlement, lesson progress, submissions and grades, score events, leaderboards, rewards, automations, payment state, minors’ data and administrative evidence.
Actors and trust boundaries: learner, guardian, instructor, evaluator, moderator, support operator, school owner and platform administrator; browser or game client ↔ application; application ↔ school membership/database/cache/ queue/storage; application ↔ billing/media/notification provider; automation or AI output ↔ authorized domain command. The client is never authoritative for identity, entitlement, grade, score, reward or payment state.
| Abuse case | Required disposition | Repository evidence or remaining work |
|---|---|---|
ACADEMY-01 forged learner, role or school context | Derive the subject and memberships from an authenticated server-side session or reviewed gateway; never trust form/header identity. | The LMS example (examples/lms, the former LMS starter) derives its numeric learner ID from the encrypted session cookie, requires that user to exist and binds it with a fixed student role into a server-created UserContext; it has no schools, memberships or tenant selector. Invite/provisioning, roles and the separate production identity lifecycle remain application work. |
ACADEMY-02 cross-user lesson or progress access | Resolve the lesson and active enrollment, authorize the enrollment owner before returning media or writing progress, and deny before side effects. | The LMS example’s (examples/lms, the former LMS starter) learning service authorizes the enrollment owner from UserContext before any database access, and its routes sit behind the authenticated middleware. The example’s own test security_tests::another_learners_lessons_are_refused (rullst-lms-example) executes that owner boundary. |
ACADEMY-03 unenrolled or expired content access | Check a current server-side entitlement on every protected lesson, download and media request; signed URLs must be short-lived and subject-bound. | Retired in v13 (TM-13.0). The full Academy blueprint that implemented this was removed; the LMS example has no enrollment release rules, expiry or signed media URLs. An application that adds them must implement and test this disposition. |
ACADEMY-04 cross-school data leak | Scope reads, counts, exports, cache keys, jobs, files, search and telemetry by authenticated school membership and test non-interference. | Retired in v13 (TM-13.0). The full Academy blueprint that implemented this was removed; the LMS example has no schools, memberships or cohorts. An application that adds them must implement and test this disposition. |
ACADEMY-05 assessment or grade tampering | Version authoritative questions/rubrics, enforce attempt/time limits server-side, grade trusted inputs and audit every override. | Retired in v13 (TM-13.0). The full Academy blueprint that implemented this was removed; the LMS example has no quizzes, assignments or grades. An application that adds them must implement and test this disposition. |
ACADEMY-06 replayed, impossible or reordered score | Authenticate a versioned ScoreEvent, recompute or validate results, enforce unique attempt/deduplication keys and deterministic leaderboard ordering. | Retired in v13 (TM-13.0). The full Academy blueprint that implemented this was removed; the LMS example has no score events or leaderboards. An application that adds them must implement and test this disposition. |
ACADEMY-07 duplicate or confused-deputy automation | Commit a transactional outbox with domain state, deliver at least once, make handlers idempotent and reauthorize sensitive actions as the recorded actor/tenant. | Retired in v13 (TM-13.0). The full Academy blueprint that implemented this was removed; the LMS example has no outbox-driven automation. An application that adds them must implement and test this disposition. |
ACADEMY-08 admin, support or AI privilege escalation | Apply least privilege and separation of duties, require step-up/approval for sensitive mutations and treat AI output only as untrusted input to guarded tools. | Retired in v13 (TM-13.0). The full Academy blueprint that implemented this was removed; the LMS example has no publication review, role grants or an AI admin. An application that adds them must implement and test this disposition. |
ACADEMY-09 malicious upload or active content | Enforce real type/size/name limits, isolate parsing, quarantine/scanning and distinct SVG/HTML/archive/document/media policies. | Core now exposes a bounded in-memory admission contract with a hard size ceiling and application allowlist, canonical tenant/name checks, exact declared MIME/extension versus recognized signature, active-text rejection (text whose first significant character after whitespace, byte-order marks or zero-width characters opens markup, including XML prologs, comments and doctypes, or that contains javascript:), tenant-prefixed randomized quarantine keys, SHA-256 binding, a static-dispatch scanner contract and fail-closed release. The deterministic scanner is explicitly mock-only. The exact negatives reject active SVG/HTML text, including BOM-, prolog- and comment-prefixed markup, traversal, invalid tenant and MIME/extension spoofing. Multipart streaming, remote S3/R2 persistence, sandboxed parsers/transcoding, archives and a production malware adapter remain open. |
ACADEMY-10 minors’ privacy or unsafe retention | Minimize collection, separate guardian consent when required, implement export/deletion/retention/anonymization and keep PII out of logs/telemetry. | Retired in v13 (TM-13.0). The full Academy blueprint that implemented this was removed; the LMS example has no age bands, guardian consent or retention workers. An application that adds them must implement and test this disposition. |
ACADEMY-11 payment-to-entitlement substitution | Derive product, school, learner and amount from server state; apply verified idempotent provider events before granting or revoking entitlement. | Capital webhook foundations exist, but the LMS example has no billing-to-entitlement integration and makes no paid-course claim. |
ACADEMY-12 availability, farming or multi-account abuse | Apply identity and origin budgets, replay controls, anomaly review and accessible rules without using a manipulable cache as source of truth. | The optional redis-rate-limit adapter provides atomic shared counters, bounded windows and hashed client keys; empty/mock_* configuration is visibly process-local and fails require_distributed(). The exact negative gate proves that fail-closed boundary, while a separate CI/release contract proves two independent clients consume one budget against digest-pinned Redis. Composition of the shared limiter into generated HTTP routes, cluster/failover and domain-specific farming/multi-account tests remain open. |
Academy negative minimum before a comparative security claim: anonymous,
cross-user, cross-course and cross-school content/progress requests; expired
entitlement; assessment replay/time manipulation; duplicate/impossible score;
automation redelivery; unauthorized grade/admin mutation; hostile upload;
payment replay; and multiple-account abuse. The exact release-negative gate now
exercises the LMS example’s owner/cross-user boundary (ACADEMY-02), hostile
upload admission (ACADEMY-09) and the explicit distributed-rate-limit startup
boundary (ACADEMY-12). Cross-school, assessment and score cases were retired
with the Academy scaffold; tenant isolation, Redis failover and domain-specific
multi-account behavior remain outside that bounded evidence.
TM-LABS-1 — trusted grading and the host-owned runner boundary
Assets: learner source, hidden grader cases and expected answers, job status and grades, the encrypted job database and content key, the receipt signing seed, and the hosts that execute submissions.
Actors and trust boundaries: learner and instructor ↔ application;
application ↔ rullst-labs shared-local job store; application-owned runner
controller ↔ job store and signing seed; controller ↔ untrusted isolated
worker. rullst-labs executes nothing. The rullst-labs-runner candidate was
removed from the 13.0 workspace, so every execution, isolation and teardown
control below the controller belongs to the application’s own runner, described
by the controller contract.
| Abuse case | Required disposition | Repository evidence or remaining work |
|---|---|---|
LABS-01 learner code runs in the web process or beside application secrets | Never execute in the HTTP process. The worker receives only bounded WorkerInput data, without identity, expected answers, keys, database, network or control sockets. | check-labs-boundary.py rejects an executor binary, an execution-engine dependency in any workspace member and a reintroduced runner crate. one_lease_and_exact_trusted_grading_without_worker_answers_or_authoritative_pass_flag checks the worker input. Worker isolation is host-owned and not verified by Rullst. |
LABS-02 forged or self-awarded grade | Accept results only under the pinned controller key; grade worker values against stored answers on the trusted side; never accept a pass flag. | pinned_receipt_key_exact_binding_and_teardown_control_experimental_grades and signed_mixed_outcomes_award_only_exact_answers_and_preserve_minimized_feedback. Signing-seed custody is host-owned. |
LABS-03 stale, replayed, cancelled or cross-job result | Bind receipts to the current lease nonce, revision and request/profile/source digests; refuse expired, cancelled or withdrawn work; treat an exact replay as an idempotent read. | cancellation_fences_late_results_and_retention_waits_for_confirmed_cleanup, withdrawal_and_expiry_prevent_a_validly_signed_late_grade and failed_controller_fences_immediately_and_old_attempt_cannot_stop_a_retry. |
LABS-04 lost worker or uncertain teardown treated as success | Fence first; require a signed WorkerLost cleanup attestation with confirmed teardown; allow at most one deliberate retry with a fresh nonce. | lost_worker_requires_cleanup_and_a_fresh_nonce_before_one_bounded_retry and the byo_runner_controller example tests. Whether teardown actually happened is host-owned. |
LABS-05 hidden cases or source disclosed | Encrypt source and graders at rest, redact Debug output, return keyed exercise digests and erase source at terminal states. | source_and_grader_are_encrypted_and_record_index_tampering_is_detected, untrusted_source_and_expected_answers_are_bounded_and_not_debug_output and returned_views_never_carry_the_unkeyed_exercise_digest. Runner-side workspaces, logs and backups are host-owned. |
LABS-06 resource exhaustion through submissions | Bound source, cases and limits; enforce store-wide and per-learner job budgets; rate-limit Submit in host authorization; enforce CPU, memory, process, disk and output limits in the runner. | one_learner_cannot_fill_the_store_wide_job_budget, submissions_that_could_never_be_claimed_are_refused and malformed_identity_and_expanded_execution_policy_are_rejected. Runtime limits are host-owned. |
Host-owned runner boundary: the removed candidate’s first-profile threat model and hosted evidence describe software Rullst no longer ships. An operator deploying a runner must extend this model with its own platform threats (sandbox and kernel escape, network and cloud-metadata reachability, cross-tenant caches, toolchain provenance, resource exhaustion and incomplete teardown), test the negatives on the deployed host and obtain independent review before any production hostile-code claim.
TM-DEPLOY-1 — CLI, artifacts and release/deployment
Assets: source, generated projects, registry token, release tag, .crate
archives, evidence, deployment credentials and production target.
| Abuse case | Required disposition | Repository evidence or remaining work |
|---|---|---|
DEPLOY-01 tag/version/DAG mismatch | Match every package/internal requirement to the tag and publish topologically. | Machine-readable order and metadata preflight. |
DEPLOY-02 artifact/source substitution | Package from tag, inspect/checksum/attest, reproduce in publish job and compare bytes. | Workflow gates exist; real RC run remains open. |
DEPLOY-03 secret/local artifact leakage | Deny .env*, key/certificate patterns, unsafe paths and unexpected archives. | .crate audit gate. |
DEPLOY-04 partial irreversible publish | Wait for index/checksum after each crate and resume only if the existing checksum matches. Never reuse a version. | Workflow exists; recovery runbook remains open. |
DEPLOY-05 CLI command/path injection | Validate identifiers, paths and image names; avoid shell interpolation and propagate failures. | Generator injection/path tests. |
DEPLOY-06 generated insecure default | Compile materialized matrices and assert production fails closed while local shortcuts are loopback debug-only. | Structural/representative blueprint matrices plus materialized auth, mail, chat and SQLx/Turso billing contracts exist; final RC/multi-OS evidence remains open. |
DEPLOY-07 compromised dependency/action | Pin actions, lock dependencies, audit advisories/licenses/sources and govern expiring exceptions. | Policy exists; tag-bound result remains open. |
Trust boundaries are contributor/CI ↔ repository, tag ↔ artifact, artifact ↔ registry and CLI ↔ filesystem/process/cloud.
Review and change control
- Changes to authentication, authorization, tenant resolution, webhooks, AI tools, OTA or release flow must reference at least one abuse-case ID in their test or review description.
- New boundaries receive new IDs; IDs are never silently reused.
- Closing residual risk requires code/configuration, a negative test and commit-bound evidence. Documentation alone cannot claim a control is deployed.
- For every v13 release, maintainers must review TM-13.0 against the exact candidate, applications must add topology/provider threats and an independent reviewer must cover the highest-impact paths.