discourse/plugins/discourse-ai/app/controllers/discourse_ai/summarization/summary_controller.rb
Roman Rizzi aa3e44b32c
FIX: State-changing summary generation is exposed via GET (CSRFable navigation) (#40232)
## Summary

State-changing summary generation/streaming is exposed via GET endpoint,
bypassing CSRF protection and allowing cross-site triggering of summary
generation and credit consumption.

## Source

- Patch Triage: https://patch.discourse.org/patch-triage/334
- Original commit:
https://github.com/discourse/discourse/blob/main/plugins/discourse-ai/app/controllers/discourse_ai/summarization/summary_controller.rb

---

🤖 Auto-generated from the patch diff via Patch Triage. Review carefully
before merging.

Co-authored-by: discourse-patch-triage
<272280883+discourse-patch-triage[bot]@users.noreply.github.com>

---------

Co-authored-by: discourse-patch-triage[bot] <272280883+discourse-patch-triage[bot]@users.noreply.github.com>
2026-05-22 13:59:21 -03:00

85 lines
2.8 KiB
Ruby
Vendored

# frozen_string_literal: true
module DiscourseAi
module Summarization
class SummaryController < ::ApplicationController
include AiCreditLimitHandler
requires_plugin PLUGIN_NAME
def show
topic = Topic.find(params[:topic_id])
guardian.ensure_can_see!(topic)
summarization_service = DiscourseAi::TopicSummarization.for(topic, current_user)
cached_summary = summarization_service.cached_summary
raise Discourse::NotFound if !cached_summary
if !guardian.can_see_summary?(topic, cached_summary: cached_summary)
raise Discourse::NotFound
end
render_serialized(cached_summary, AiTopicSummarySerializer)
end
def create
topic = Topic.find(params[:topic_id])
guardian.ensure_can_see!(topic)
summarization_service = DiscourseAi::TopicSummarization.for(topic, current_user)
cached_summary = summarization_service.cached_summary
if !guardian.can_see_summary?(topic, cached_summary: cached_summary)
raise Discourse::NotFound
end
RateLimiter.new(current_user, "summary", 6, 5.minutes).performed! if current_user
opts = params.permit(:skip_age_check)
skip_age_check = opts[:skip_age_check] == "true"
if params[:stream] && current_user
if cached_summary && !skip_age_check
render_serialized(cached_summary, AiTopicSummarySerializer)
return
end
Jobs.enqueue(
:stream_topic_ai_summary,
topic_id: topic.id,
user_id: current_user.id,
skip_age_check: skip_age_check,
)
render json: success_json
else
hijack do
summary = summarization_service.summarize(skip_age_check: skip_age_check)
raise Discourse::NotFound if summary.nil?
render_serialized(summary, AiTopicSummarySerializer)
end
end
end
def regen_gist
RegenerateSummaries.call(**service_params, params: params.merge(type: "gist")) do
on_success { render json: success_json }
on_failed_policy(:can_regenerate) { raise Discourse::InvalidAccess }
on_failed_contract do |contract|
raise Discourse::InvalidParameters, contract.errors.full_messages.join(", ")
end
end
end
def regen_summary
RegenerateSummaries.call(**service_params, params: params.merge(type: "summary")) do
on_success { render json: success_json }
on_failed_policy(:can_regenerate) { raise Discourse::InvalidAccess }
on_failed_contract do |contract|
raise Discourse::InvalidParameters, contract.errors.full_messages.join(", ")
end
end
end
end
end
end