|
95 | 95 |
|
96 | 96 | 14. ~~**`.live()` subscriptions**~~ — done (PR #72). Predicate extraction, |
97 | 97 | reverse-index bus, MVCC-snapshot-aware re-iteration. |
| 98 | + |
| 99 | +16. **Cross-database provenance for typed values (RPC security).** Runtime |
| 100 | + validators today check `v instanceof SqlValue` — "is this a typegres |
| 101 | + value?" but not "*whose* typegres value?" Over RPC (exoeval), a client |
| 102 | + can construct a `Bool` (or any typed expression) and hand it to |
| 103 | + `db.execute(qb.where(theirBool))`. The check passes if the object is |
| 104 | + a proper SqlValue instance; nothing verifies it was built *for the |
| 105 | + Database the RPC handler is bound to*. |
| 106 | + |
| 107 | + Threats: |
| 108 | + |
| 109 | + - **Cross-dialect smuggling:** attacker on a SQLite-backed session |
| 110 | + injects a Bool constructed against a Postgres schema. SQL emission |
| 111 | + collides (`?` vs `$N` placeholders, wrong typenames). Coarsely |
| 112 | + addressed by the dialect check that comes with the `Any` → `SqlValue` |
| 113 | + sweep (Table+QB dialect-agnostic work), because `v.constructor.dialect.name` |
| 114 | + is authoritative per class. |
| 115 | + - **Cross-tenant smuggling** (same dialect, different DB instances): |
| 116 | + values from tenant B's schema get spliced into tenant A's query. |
| 117 | + Column names / OIDs matching by coincidence → info leak. Dialect |
| 118 | + check does *not* catch this. |
| 119 | + - **Cross-session smuggling** within one Database: some values carry |
| 120 | + session-scoped state via `db.scope(principal)`. Bypassing the tag = |
| 121 | + privilege escalation. |
| 122 | + |
| 123 | + Design options: |
| 124 | + |
| 125 | + - **Instance-scoped tagging.** Each `Database` mints a `Symbol()`; |
| 126 | + values built through a db-scoped factory (`db.Int4.from(5)` instead |
| 127 | + of `Int4.from(5)`) carry that tag. Runtime checks `v[dbIdKey] === this.dbId`. |
| 128 | + Catches all three. Cost: breaking API change for existing PG callers; |
| 129 | + RPC serialization needs to preserve the tag across the wire. |
| 130 | + - **Scope-only.** Don't tag values; validate the compiled Sql tree at |
| 131 | + `db.execute()` time — walk it and reject any SqlValue whose dialect |
| 132 | + doesn't match ctx. Cheap, addresses cross-dialect only. |
| 133 | + - **Opt-in session tagging.** Untagged values keep working (interop). |
| 134 | + `db.scope(session).typedValue(x)` explicitly binds. Session-sensitive |
| 135 | + methods (mutation, sensitive-table `.where`) refuse untagged values. |
| 136 | + Backwards-compatible; enforcement lives where the security matters. |
| 137 | + |
| 138 | + Operator/method checks have the same story — `int.plus(other)` today |
| 139 | + accepts any Int4-ish arg; under instance-tagging, `runtime.match()` |
| 140 | + would enforce the tag matches `this`. Single code path to protect. |
| 141 | + |
| 142 | + Not blocking Phase 1 (SQLite dialect work), but *is* blocking any RPC |
| 143 | + production deployment with multi-tenant or cross-dialect setups. Fold |
| 144 | + into the exoeval hardening pass (relates to #13 gas accounting — both |
| 145 | + are RPC-boundary threats). |
0 commit comments