[하루한줄] CVE-2025-48062: Discourse의 Topic 초대 시 전송되는 이메일을 통한 HTML Injection 취약점
Target
- Discourse < 3.4.5
- Discourse < 3.5.0.beta6
Explain
background
Discourse는 오픈소스 기반 커뮤니티 플랫폼으로 GitLab, OpenAI 등에서 사용 중인 서비스입니다. CVE-2025-48062 취약점은 사용자에게 Topic 초대를 위한 이메일 전송 시 포함되는 내용이 별도의 필터링 없이 삽입되어 HTML Injection을 유발합니다.
root cause
새로운 Topic 초대 메일 생성 요청이 들어오면 라우팅 규칙에 따라 app/controllers/invites_controller.rb 파일의 create()
함수에서 처리됩니다.
# config/routes.rb
resources :invites, only: %i[create update destroy]
get "/invites/:id" => "invites#show", :constraints => { format: :html }
post "invites/create-multiple" => "invites#create_multiple", :constraints => { format: :json }
# app/controllers/invites_controller.rb
def create
begin
if params[:topic_id].present?
topic = Topic.find_by(id: params[:topic_id]) # ----------> Find Topic by topic_id
raise Discourse::InvalidParameters.new(:topic_id) if topic.blank?
guardian.ensure_can_invite_to!(topic)
end
# ...
invite =
Invite.generate( # ------------> Generate mail content
current_user,
email: params[:email],
domain: params[:domain],
skip_email: params[:skip_email],
invited_by: current_user,
custom_message: params[:custom_message],
max_redemptions_allowed: params[:max_redemptions_allowed],
topic_id: topic&.id,
group_ids: groups&.map(&:id),
expires_at: params[:expires_at],
invite_to_topic: params[:invite_to_topic],
)
이때 전달 받는 파라미터는 topic_id
로 Topic.find_by()
호출을 통해 ActiveRecord 상에 일치하는 데이터가 존재하는지 확인합니다. Topic이 정상적으로 존재하는 경우 Invite.generate()
호출을 통한 email body 생성을 진행합니다.
위 흐름도는 컨트롤러부터 실제 body 내용을 작성하는 body()
함수까지의 전체적인 흐름입니다.
# lib/email/message_builder.rb
def body
body = nil
if @opts[:template]
body = I18n.t("#{@opts[:template]}.text_body_template", template_args).dup # --> Set body content
else
body = @opts[:body].dup
end
if @template_args[:unsubscribe_instructions].present?
body << "\n"
body << @template_args[:unsubscribe_instructions]
end
DiscoursePluginRegistry.apply_modifier(:message_builder_body, body, @opts, @to)
end
함수를 살펴보면 별도의 필터링 과정 없이 @template_args
로 넘어온 값을 body
내용에 포함합니다. 마찬가지로 상위 단계에서 이를 호출할 때에도 유효한 데이터가 존재하는지 검증을 진행할 뿐, 악성 콘텐츠 포함 여부는 검증하지 않습니다.
# app/mailers/invite_mailer.rb
if invite_to_topic && first_topic.present?
# get topic excerpt
topic_excerpt = ""
topic_excerpt = first_topic.excerpt.tr("\n", " ") if first_topic.excerpt
topic_title = first_topic.try(:title) # ----> Get topic_title
if SiteSetting.private_email?
topic_title = I18n.t("system_messages.private_topic_title", id: first_topic.id)
topic_excerpt = ""
end
patch diffing
3.5.0.beta6에서 적용된 패치를 확인하기 위해 3.5.0.beta5와 비교를 진행하면 commit 기록 중 “SECURITY: Escape topic title for mailers”를 확인할 수 있습니다. 해당 commit hash는 72e224b7627b410a00afdd3fb185b3523518dadc
로 ‘message_builder.rb’와 ‘message_builder_spec.rb’ 파일에서 수정이 이루어졌습니다.
‘message_builder_spec.rb’ 파일은 구현된 기능이 올바르게 동작하는지 확인하기 위한 테스트 파일로 실제 구현에는 영향을 미치지 않습니다. 관련 내용은 RSpec에서 참고할 수 있습니다.
# lib/email/message_builder.rb
def body
body = nil
if @opts[:template]
template_args_to_escape = %i[topic_title inviter_name]
template_args_to_escape.each do |key|
next if !@template_args.key?(key)
@template_args[key] = escaped_template_arg(key) # --> new mitigation function is called
end
# ...
private
def escaped_template_arg(key) # -----> new mitigation function
value = template_args[key].dup
# explicitly escaped twice, as Mailers will mark the body as html_safe
once_escaped = String.new(ERB::Util.html_escape(value))
ERB::Util.html_escape(once_escaped)
end
변경된 코드를 확인하면 body()
함수의 처리 과정 중 새로 추가된 escaped_template_arg()
함수를 호출하여 악의적인 내용이 포함되지 못하도록 수정되었습니다.
Exploit
requirements
공격자는 피해자에게 초대 메일을 보내기 위해 ActiveRecord 상에 유효한 토픽 내용이 존재해야 합니다. Discourse에서 토픽은 일반적으로 새로운 게시글을 만드는 것과 동일한 개념이기 때문에 만약 이 과정 중 특정 문자열에 대한 검사가 있는 경우 원활한 exploit이 불가합니다.
limitations
Topic 생성 과정을 확인하면 사이트 설정에 따른 최대 길이를 검증하는 로직이 존재합니다.
# app/assets/javascripts/discourse/app/services/composer.js
if (
opts.topicTitle &&
opts.topicTitle.length <= this.siteSettings.max_topic_title_length
) {
this.model.set("title", opts.topicTitle);
}
따라서 payload 작성 시 길이 제한은 존재할 수 있지만 이외에 추가적인 검증은 확인되지 않았습니다.
Takeaways
최근 많은 브라우저나 메일 클라이언트의 경우 client-side 공격에 대한 보호를 제공하지만 위와 같은 공격은 불특정 다수에게 악성 메일을 전송하는 것으로 광범위한 피해를 입힐 수 있습니다. 이를 예방하기 위해 여러 프레임워크에서 escape 함수 또한 제공하는 만큼 적극 활용하는 것이 좋을 것 같습니다.
References
본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.