Secure Electric shape proxy: default-deny tables + validate client where#4549
Open
KyleAMathews wants to merge 2 commits into
Open
Secure Electric shape proxy: default-deny tables + validate client where#4549KyleAMathews wants to merge 2 commits into
KyleAMathews wants to merge 2 commits into
Conversation
The /_electric/electric/v1/shape proxy injected the privileged Electric secret into every request, then scoped only a fixed set of tables; any other table fell through and was forwarded with the secret and no row/ column filter. It also AND-combined the client `where` as `enforced AND (clientWhere)`, which a payload like `1=1) OR (1=1` could break out of under SQL precedence, defeating per-tenant/per-principal scoping. - Default-deny: reject (403 TABLE_NOT_ALLOWED) any table not in the explicitly scoped allowlist, or a missing table param. - Reject (400 INVALID_WHERE) client where clauses that are not self-contained: unbalanced parens, top-level paren underflow, unterminated string/identifier literals, or SQL comment markers. - Map ElectricProxyError to an HTTP response in errorMapper and log rejections for intrusion detection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4549 +/- ##
===========================================
+ Coverage 54.80% 74.13% +19.33%
===========================================
Files 317 52 -265
Lines 36681 6933 -29748
Branches 10466 2204 -8262
===========================================
- Hits 20104 5140 -14964
+ Misses 16544 1777 -14767
+ Partials 33 16 -17
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Hardens the agents-server Electric shape proxy (
/_electric/electric/v1/shape) against two access-control bypasses. Previously, any authenticated principal could read any table — and sidestep per-tenant/per-principal row scoping — through the proxy. Now unscoped tables are denied and clientwhereclauses that could escape the enforced scope are rejected.Root Cause
The proxy injects the privileged Electric API secret into every
/v1/shaperequest, then applied scoping only via anif/else-ifchain over ~9 known tables. Two gaps:return target— forwarded upstream with the secret attached and no row/column filter. A low-privilege principal could requesttable=entity_permission_grants,subscription_webhooks, etc. and read the whole table (cross-tenant in a shared deployment).where-clause break-out. Scoping was combined asenforced AND (clientWhere). Since the client controls the text inside the parens and SQLANDbinds tighter thanOR, a payload like1=1) OR (1=1collapses to(enforced AND (1=1)) OR (1=1)→ always true, defeating the scoping.Approach
if/else-ifnow ends in anelsethat throwsElectricProxyError(403, TABLE_NOT_ALLOWED). Only the explicitly column+row-scoped tables (the exact set the UI, mobile, and runtime sync) can be proxied; a missingtableis also rejected. Verified no legitimate consumer requests a table outside this set (wake_registrationsconnects to Electric directly, not via the proxy).wherevalidation:isSelfContainedWhereClauserejects (400 INVALID_WHERE) any client clause whose parentheses dip below the top level or are unbalanced, has unterminated'/"literals, or contains--//* */comment markers — a single-pass scanner that ignores characters inside string/identifier literals (with''/""escapes).ElectricProxyErrorlives inserver-utils.ts(kept free of the heavyentity-managerimport graph) and is mapped to an HTTP response by the globalerrorMapper, which now also logs rejections for intrusion detection.Key Invariants
/v1/shaperequest resolves to an explicitly scoped table, or it is rejected before forwarding (the secret is never attached to an unscoped request that reaches upstream).wherethat passes validation cannot break out of the(...)group it is wrapped in — soenforced AND (client)always keepsenforcedin force.E''strings are intentionally unmodeled and fall into over-rejection.Non-goals
if/else-iftable chain to aswitch— out of scope, would bury the security change.wherebeyond structural self-containment; the upstream Postgres parser remains the authority on validity.Trade-offs
A structural scanner (vs. a full SQL parser) is simpler and safe because the only way to escape the wrapping parens is an unbalanced top-level
), which the depth check catches; comment markers are rejected outright to avoid the trailing-paren footgun. ReusingElectricAgentsErrorwas rejected to avoid pulling the heavyentity-managergraph into a utils module (keeps the unit test fast); the class instead mirrors its shape and is mapped uniformly inerrorMapper.Verification
Files changed
packages/agents-server/src/utils/server-utils.ts—ElectricProxyErrorclass,isSelfContainedWhereClausevalidator, default-deny branch +wherevalidation inbuildElectricProxyTarget.packages/agents-server/src/routing/hooks.ts—errorMappermapsElectricProxyErrorto an HTTP response and logs the rejection.packages/agents-server/test/server-utils.test.ts,test/routing-hooks.test.ts— security test coverage.🤖 Generated with Claude Code