From 208e932121b2e57ff28f879c197fa660d14b2e21 Mon Sep 17 00:00:00 2001 From: Morgan Roderick Date: Thu, 11 Jun 2026 10:21:20 +0200 Subject: [PATCH] fix: skip CSRF protection for feedback form submission MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The feedback form uses a unique secret token in the URL to authenticate the request. This is sufficient protection against CSRF — an attacker would need to know the token to submit the form. However, protect_from_forgery requires a session cookie, which browsers like Safari withhold when they classify the request as cross-site (e.g. when a user navigates from a third-party app or Intelligent Tracking Prevention is active). This causes the form submission to fail with ActionController::InvalidAuthenticityToken even for legitimate users. This has caused 82 occurrences in production (Rollbar #535). Changes: - Skip CSRF protection on FeedbackController#submit - Add controller specs covering show, submit, and the CSRF-exempt path Fixes: https://app.rollbar.com/a/codebar-production/fix/item/codebar-production/535#detail --- app/controllers/feedback_controller.rb | 2 + spec/controllers/feedback_controller_spec.rb | 92 ++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 spec/controllers/feedback_controller_spec.rb diff --git a/app/controllers/feedback_controller.rb b/app/controllers/feedback_controller.rb index 7873617f0..416fe2034 100644 --- a/app/controllers/feedback_controller.rb +++ b/app/controllers/feedback_controller.rb @@ -1,4 +1,6 @@ class FeedbackController < ApplicationController + skip_forgery_protection only: :submit + def show feedback_request = FeedbackRequest.find_by(token: params[:id], submited: false) diff --git a/spec/controllers/feedback_controller_spec.rb b/spec/controllers/feedback_controller_spec.rb new file mode 100644 index 000000000..cdac0bfd2 --- /dev/null +++ b/spec/controllers/feedback_controller_spec.rb @@ -0,0 +1,92 @@ +RSpec.describe FeedbackController do + let(:feedback_request) { Fabricate(:feedback_request) } + let(:coach) { Fabricate(:coach) } + let(:tutorial) { Fabricate(:tutorial) } + + describe 'GET #show' do + context 'with a valid token' do + it 'renders the feedback form' do + get :show, params: { id: feedback_request.token } + expect(response).to have_http_status(:success) + end + end + + context 'with an already submitted token' do + it 'redirects to the root path' do + submitted_request = Fabricate(:feedback_request, submited: true) + get :show, params: { id: submitted_request.token } + expect(response).to redirect_to(root_path) + end + end + end + + describe 'PATCH #submit' do + before do + Fabricate(:attended_workshop_invitation, workshop: feedback_request.workshop, member: coach, role: 'Coach') + end + + context 'with valid data' do + it 'saves the feedback and redirects to the root path' do + patch :submit, params: { + id: feedback_request.token, + feedback: { + coach_id: coach.id, + tutorial_id: tutorial.id, + request: 'It was great!', + rating: 5, + suggestions: '' + } + } + + expect(response).to redirect_to(root_path) + expect(flash[:notice]).to eq(I18n.t('messages.feedback_saved')) + expect(feedback_request.reload.submited).to be true + end + end + + context 'with invalid data' do + it 'renders the feedback form with errors' do + patch :submit, params: { + id: feedback_request.token, + feedback: { + coach_id: coach.id, + tutorial_id: tutorial.id, + request: '', + rating: nil, + suggestions: '' + } + } + + expect(response).to have_http_status(:success) + expect(flash[:alert]).to include("Rating can't be blank") + end + end + + context 'without a CSRF token (browser did not send session cookie)' do + it 'still accepts the feedback submission' do + # Simulate the real-world scenario where the browser withholds the + # session cookie (e.g. Safari ITP, cross-site navigation, or cookie + # blocking). With protect_from_forgery enabled, this would normally + # raise ActionController::InvalidAuthenticityToken. + ActionController::Base.allow_forgery_protection = true + + patch :submit, params: { + id: feedback_request.token, + feedback: { + coach_id: coach.id, + tutorial_id: tutorial.id, + request: 'It was great!', + rating: 5, + suggestions: '' + } + } + + expect(response).to redirect_to(root_path) + expect(flash[:notice]).to eq(I18n.t('messages.feedback_saved')) + expect(feedback_request.reload.submited).to be true + ensure + ActionController::Base.allow_forgery_protection = false + end + end + end +end