Skip to content

Fix authentication bypass on /v1/shape via path-normalization mismatch - #4765

Merged
alco merged 4 commits into
mainfrom
fix/shape-auth-path-normalization-bypass
Aug 20, 2026
Merged

Fix authentication bypass on /v1/shape via path-normalization mismatch#4765
alco merged 4 commits into
mainfrom
fix/shape-auth-path-normalization-bypass

Conversation

@alco

@alco alco commented Aug 20, 2026

Copy link
Copy Markdown
Member

Summary

Fixes an authentication bypass on /v1/shape in secure mode (private advisory GHSA-m4f6-p2j8-rq76).

authenticate/2 guarded on conn.request_path — the raw request target — while plug :match dispatches on the percent-decoded, empty-segment-stripped path. Any request whose decoded path routes to /v1/shape but whose raw target is not exactly "/v1/shape" was dispatched to ServeShapePlug / DeleteShapePlug while falling through the unauthenticated catch-all clause, bypassing secure mode entirely — no credential required:

GET /v1/shape      -> 401 Unauthorized   (enforced)
GET /v1/shape/     -> 200                 (bypass)
GET /v1//shape     -> 200                 (bypass)
GET /v1/%73hape    -> 200                 (bypass, %73 = "s")

Affects GET, POST, DELETE and HEAD; OPTIONS is exempt by design. Only secure-mode deployments (secret != nil) are affected.

Fix

  • authenticate/2 now matches on the route the router already resolved (conn.private.plug_route, set by plug :match before the later plugs run) instead of the raw request_path. This authenticates exactly what dispatch will serve, for every encoding and slash variant and on every method, with OPTIONS still exempt and unmatched routes still reaching the 404 catch-all.
  • put_cors_headers/2 shared the same latent weakness — it keyed on the never-decoded path_info — and is switched to the same plug_route basis so CORS headers follow the resolved route.

Credit

Reported by Yahya Jirari, Beeldi (GHSA-m4f6-p2j8-rq76).

🤖 Generated with Claude Code

alco and others added 3 commits August 20, 2026 23:57
`authenticate/2` guarded on `conn.request_path` (the raw request target)
while `plug :match` dispatches on the percent-decoded, empty-segment-stripped
path. Any request whose decoded path routes to `/v1/shape` but whose raw
target is not exactly "/v1/shape" — e.g. "/v1/shape/", "/v1//shape",
"/v1/%73hape" — was dispatched to the shape plugs while falling through the
unauthenticated catch-all clause, bypassing secure mode entirely.

Gate on the route the router already resolved (`conn.private.plug_route`,
set by `plug :match` before the later plugs run) so authentication covers
exactly what dispatch will serve, for every encoding and slash variant and
on every method, with OPTIONS still exempt and unmatched routes still
reaching the 404 catch-all.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VSXLrB24RAKuYBC5oQ1Svy
`put_cors_headers/2` keyed on `conn.path_info`, which Plug never decodes
(the decode happens inside `match/2` and is not written back). A request
such as "/v1/%73hape" therefore routes to the shape handler but missed the
shape CORS clause and received the default GET/HEAD method list. Match on
`conn.private.plug_route` — the same resolved-route basis now used by
`authenticate/2` — so CORS headers follow the route dispatch actually
selected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VSXLrB24RAKuYBC5oQ1Svy
Guards the non-security direction of the plug_route-based auth gate: in
insecure mode, requests whose target normalizes to "/v1/shape" (trailing
slash, doubled slash, percent-encoded) must still be dispatched to the shape
handler and served, not over-blocked or lost to the 404 catch-all.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VSXLrB24RAKuYBC5oQ1Svy
@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 60.02%. Comparing base (77a22fa) to head (f107960).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4765      +/-   ##
==========================================
- Coverage   60.06%   60.02%   -0.05%     
==========================================
  Files         397      397              
  Lines       43772    43772              
  Branches    12591    12589       -2     
==========================================
- Hits        26293    26272      -21     
- Misses      17398    17418      +20     
- Partials       81       82       +1     
Flag Coverage Δ
packages/agents 72.64% <ø> (ø)
packages/agents-mcp 77.70% <ø> (ø)
packages/agents-mobile 80.67% <ø> (ø)
packages/agents-runtime 83.73% <ø> (-0.02%) ⬇️
packages/agents-server 75.45% <ø> (-0.25%) ⬇️
packages/agents-server-ui 8.32% <ø> (ø)
packages/electric-ax 51.06% <ø> (ø)
packages/experimental 87.73% <ø> (ø)
packages/react-hooks 86.48% <ø> (ø)
packages/start 82.83% <ø> (ø)
packages/typescript-client 91.83% <ø> (-0.12%) ⬇️
packages/y-electric 56.05% <ø> (ø)
typescript 60.02% <ø> (-0.05%) ⬇️
unit-tests 60.02% <ø> (-0.05%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@netlify

netlify Bot commented Aug 20, 2026

Copy link
Copy Markdown

Deploy Preview for electric-next ready!

Name Link
🔨 Latest commit f107960
🔍 Latest deploy log https://app.netlify.com/projects/electric-next/deploys/6a877c1b2e24220009e85ac6
😎 Deploy Preview https://deploy-preview-4765--electric-next.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@alco
alco merged commit c9e8a68 into main Aug 20, 2026
81 of 85 checks passed
@alco
alco deleted the fix/shape-auth-path-normalization-bypass branch August 20, 2026 22:35
@github-actions

Copy link
Copy Markdown
Contributor

This PR has been released! 🚀

The following packages include changes from this PR:

  • @core/sync-service@1.7.12

Thanks for contributing to Electric!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants