mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-06-19 05:59:26 +08:00
The AI 'Explain' feature builds a prompt that includes the reply-to post's raw content, but only checks `can_see?` on the post being explained, not the parent. If post B replies to a hidden post A, a user who triggers explain on post B gets post A's content fed into the prompt (potentially surfaced in the AI response). This is a problem if the hidden post contains sensitive content like credentials that were hidden by a mod but not yet redacted. This PR ensures we check replies are also `can_see?` and escapes interpolated values with `ERB::Util.html_escape` to prevent post content from breaking out of the prompt's XML tag structure (e.g. raw containing `</context><replyTo>injected</replyTo>`) <img width="600" alt="Screenshot 2026-04-29 at 6 32 12 PM" src="https://github.com/user-attachments/assets/e1cd535b-bd0f-4411-ab34-582d66fbf518" /> https://github.com/discourse/discourse/security/advisories/GHSA-7h76-fwxc-j586
73 lines
2 KiB
Ruby
Vendored
73 lines
2 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
module Jobs
|
|
class StreamPostHelper < ::Jobs::Base
|
|
sidekiq_options retry: false
|
|
|
|
def execute(args)
|
|
return unless post = Post.includes(:topic).find_by(id: args[:post_id])
|
|
return unless user = User.find_by(id: args[:user_id])
|
|
return unless args[:text]
|
|
return unless args[:progress_channel]
|
|
return unless args[:client_id]
|
|
|
|
topic = post.topic
|
|
reply_to = post.reply_to_post
|
|
guardian = user.guardian
|
|
|
|
return unless guardian.can_see?(post)
|
|
|
|
helper_mode = args[:prompt]
|
|
|
|
if helper_mode == DiscourseAi::AiHelper::Assistant::EXPLAIN
|
|
input = <<~TEXT.strip
|
|
<term>#{escape_prompt_tag_value(args[:text])}</term>
|
|
<context>#{escape_prompt_tag_value(post.raw)}</context>
|
|
<topic>#{escape_prompt_tag_value(topic.title)}</topic>
|
|
#{reply_to && guardian.can_see?(reply_to) ? "<replyTo>#{escape_prompt_tag_value(reply_to.raw)}</replyTo>" : nil}
|
|
TEXT
|
|
else
|
|
input = args[:text]
|
|
end
|
|
|
|
begin
|
|
DiscourseAi::AiHelper::Assistant.new.stream_prompt(
|
|
helper_mode,
|
|
input,
|
|
user,
|
|
args[:progress_channel],
|
|
custom_prompt: args[:custom_prompt],
|
|
client_id: args[:client_id],
|
|
)
|
|
rescue LlmCreditAllocation::CreditLimitExceeded => e
|
|
publish_error(args[:progress_channel], user, e)
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def escape_prompt_tag_value(value)
|
|
ERB::Util.html_escape(value.to_s)
|
|
end
|
|
|
|
def publish_error(channel, user, exception)
|
|
allocation = exception.allocation
|
|
|
|
details = {}
|
|
if allocation
|
|
details[:reset_time_relative] = allocation.relative_reset_time
|
|
details[:reset_time_absolute] = allocation.formatted_reset_time
|
|
end
|
|
|
|
payload = {
|
|
error: true,
|
|
error_type: "credit_limit_exceeded",
|
|
message: exception.message,
|
|
details: details,
|
|
done: true,
|
|
}
|
|
|
|
MessageBus.publish(channel, payload, user_ids: [user.id], max_backlog_age: 60)
|
|
end
|
|
end
|
|
end
|