LongPoll session tokens are not bound to the mount path or socket handler
Maintainers usually reply within 1 day
@SteffenDE is already working on this.
Since Aug 21, 2026.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 52/100
Research direction
The issue names no source files or tests; start by running the linked phoenix-longpoll-mount-binding-repro and reading the longpoll token signing and resumption paths for the two socket mounts. Done means a token from one mount is rejected by the other, the appropriate socket handler is used, and each mount's max_age remains enforced.
Written by the indexing model from the issue text.
Description
Environment
- Elixir version (elixir -v): any
- Phoenix version (mix deps): 1.8.9
- Operating system: any
Actual behavior
A longpoll session token is signed as {:v1, endpoint_id, server_pid, priv_topic}. Neither the payload nor the salt includes the mounted path or the socket handler. Both are identical for every socket mounted on the endpoint.
A token from one longpoll mount is accepted at any other longpoll mount on the same endpoint and the resuming mount's handler is never consulted.
Repro:
- Clone https://github.com/lukaszsamson/phoenix-longpoll-mount-binding-repro
- mix deps.get
- mix phx.server
- open localhost:4000 and click "Run all steps"
The endpoint mounts two sockets, longpoll only:
socket "/public", G6ReproWeb.PublicSocket, websocket: false, longpoll: [...]
socket "/private", G6ReproWeb.PrivateSocket, websocket: false, longpoll: [...]
PublicSocket.connect/3 accepts everything. PrivateSocket.connect/3 always returns :error. Both serve the same room:* channel, which tags each pushed message with served_by naming the socket module that owns the session.
Result:
--- CONTROL: GET /private/longpoll (no token) ---
status: 403 <- PrivateSocket rejects all connects
--- 1. GET /public/longpoll (no token) ---
status: 410, token: SFMyNTY.g2gDaAR3AnYx...
--- 2. POST /public/longpoll (phx_join room:lobby) ---
status: 200
--- 3. POST /private/longpoll with the /public token (ping) ---
status: 200 <- accepted
--- 4. GET /private/longpoll with the /public token ---
event=phx_reply payload={"status":"ok","response":{}}
event=joined payload={"served_by":"PublicSocket (/public)"}
event=pong payload={"served_by":"PublicSocket (/public)"}
Another consequence of the bug is that :max_age is taken from the resuming mount. Given:
socket "/shortlived", MySocket, longpoll: [crypto: [max_age: 1]]
socket "/public", MySocket, longpoll: [] # default max_age: 1_209_600
a token that /shortlived rejects as expired is still accepted at /public.
1. mint at /shortlived -> 410 + token
2. POST /shortlived with token -> 200 (fresh)
3. sleep 1.5s (past max_age, session process still alive)
4. POST /shortlived with token -> 410 (own mount rejects it)
5. POST /public with the same token -> 200 (accepted anyway)
Note: I do not think this is a vulnerability. The token is for a session the caller already established. Replaying it at another mount returns the same session so no barriers are actually crossed. check_origin is still applied per request so no CSWSH bypass. The token is not a cookie so no CSRF / confused-deputy
Expected behavior
A session token should only be usable at the mount that issued it. The handler and the mount path should be included in the payload or salt.
If the behavior is intentional, the effect on :max_age should be documented
- Dominant language
- Elixir
- Stars
- 23.2k
- Forks
- 3.2k
- Avg merge
- 16h 35m
- Merged PRs (30d)
- 18
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from phoenixframework/phoenix
-
Binary pushes don't work over the LongPoll transportPossibly taken @mandar1045 claimed this 1 day ago. Open
Difficulty 3/5 1-2 days Newbie friendliness 35/100
phoenixframework/phoenix#6906 ·
Maintainers usually reply within 1 day
-
Difficulty 3/5 1-2 days Newbie friendliness 56/100
phoenixframework/phoenix#6898 · 3 comments ·
Maintainers usually reply within 1 day
-
~p treats routes as static when they only start with a static directory namePossibly taken @mandar1045 claimed this 12 days ago. Open
Difficulty 3/5 1-2 days Newbie friendliness 30/100
phoenixframework/phoenix#6895 ·
Maintainers usually reply within 1 day
-
Connection: close header not sent before closing HTTP 1.1 connection when a controller raises an exceptionPossibly taken @javiergarea claimed this 41 days ago. Open
Difficulty 4/5 3-5 days Newbie friendliness 55/100
phoenixframework/phoenix#6859 ·
Maintainers usually reply within 1 day
-
Difficulty 4/5 3-5 days Newbie friendliness 55/100
phoenixframework/phoenix#6853 · 4 comments ·
Maintainers usually reply within 1 day
All issues in phoenixframework/phoenix
Similar issues
-
Bug: Table layout and date column are cut off on mobile screens on the Tournaments -> History pageOpenbug
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
hexlet-codebattle/codebattle#2375 ·
-
Bug
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
wintermeyer/vutuv#2388 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
-
Broadcast API docs say to prepend realtime: to the topic, which breaks deliveryPossibly taken @mandar1045 claimed this 1 day ago. Open
Difficulty 1/5 Under an hour Newbie friendliness 80/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
phoenixframework/phoenix_live_view#4509 ·
Maintainers usually reply within 1 day