`GET` endpoints for state-changing operations `/projects/delete`, `/ranked/delete`, `/responses/detach` are exposed to CSRF attacks
#268 opened on May 11, 2026
Repository metrics
- Stars
- (24 stars)
- PR merge metrics
- (PR metrics pending)
Description
What happens
Several state-changing endpoints in 0rsk.rb are routed as get rather than post, with no CSRF token, no Origin/Referer validation, and no idempotency guard:
get '/projects/delete' do
projects.get(params[:id].to_i).delete
redirect '/projects'
end
get '/ranked/delete' do
# destructive — deletes a ranked triple
end
get '/responses/detach' do
# destructive — detaches a plan from a response
end
Authentication is via the glogin cookie set in front/front_login.rb:31. Because Sinatra's default Rack::Protection::HttpOrigin only inspects POST/PUT/DELETE requests, a get to a destructive route is not covered. An attacker controlling evil.com can therefore trigger any of these deletions by embedding a single <img src="https://www.0rsk.com/projects/delete?id=42"> on a page a logged-in user opens. The browser attaches the glogin cookie, and the destructive side effect executes silently.
This is a CSRF vulnerability on the GET-based state-changing endpoints. It is the same authorization-binding defect family as the accepted IDOR #263 — the cookie identifies the user but the request shape carries no proof that the user actually intended the action.
What should happen
Two changes — either alone closes the hole, both together is best:
-
Convert each destructive route from
gettopost(ordelete) so it is covered byRack::Protection::HttpOriginand by any future CSRF-token middleware. Update the corresponding view links to submit a one-button<form method="post">instead of an<a>. -
Enable
Rack::Protection::AuthenticityToken(requiresenable :sessionsplus a token embedded in every state-changing form), so every state-changing request — regardless of HTTP verb — carries a same-site secret that an attacker onevil.comcannot produce.
Result: an <img> tag on a hostile page can no longer trigger account-state changes against a logged-in 0rsk user, and the cookie no longer doubles as standing authorization for every URL the browser can be tricked into fetching.