DEV: Block accidental serialization of entire AR models (#27668)

This commit is contained in:
Jan Cernik 2024-07-01 17:08:48 -03:00 committed by GitHub
parent 1ae902fa60
commit 6599b85a75
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 98 additions and 45 deletions

View File

@ -83,7 +83,7 @@ class Admin::ThemesController < Admin::AdminController
if @theme.save if @theme.save
log_theme_change(nil, @theme) log_theme_change(nil, @theme)
render json: @theme, status: :created render json: serialize_data(@theme, ThemeSerializer), status: :created
else else
render json: @theme.errors, status: :unprocessable_entity render json: @theme.errors, status: :unprocessable_entity
end end
@ -113,7 +113,7 @@ class Admin::ThemesController < Admin::AdminController
@theme = @theme =
RemoteTheme.import_theme(remote, theme_user, private_key: private_key, branch: branch) RemoteTheme.import_theme(remote, theme_user, private_key: private_key, branch: branch)
render json: @theme, status: :created render json: serialize_data(@theme, ThemeSerializer), status: :created
rescue RemoteTheme::ImportError => e rescue RemoteTheme::ImportError => e
if params[:force] if params[:force]
theme_name = params[:remote].gsub(/.git\z/, "").split("/").last theme_name = params[:remote].gsub(/.git\z/, "").split("/").last
@ -128,7 +128,7 @@ class Admin::ThemesController < Admin::AdminController
@theme.remote_theme = remote_theme @theme.remote_theme = remote_theme
@theme.save! @theme.save!
render json: @theme, status: :created render json: serialize_data(@theme, ThemeSerializer), status: :created
else else
render_json_error e.message render_json_error e.message
end end
@ -156,7 +156,7 @@ class Admin::ThemesController < Admin::AdminController
) )
log_theme_change(nil, @theme) log_theme_change(nil, @theme)
render json: @theme, status: :created render json: serialize_data(@theme, ThemeSerializer), status: :created
rescue RemoteTheme::ImportError => e rescue RemoteTheme::ImportError => e
render_json_error e.message render_json_error e.message
end end
@ -200,7 +200,7 @@ class Admin::ThemesController < Admin::AdminController
if @theme.save if @theme.save
update_default_theme update_default_theme
log_theme_change(nil, @theme) log_theme_change(nil, @theme)
format.json { render json: @theme, status: :created } format.json { render json: serialize_data(@theme, ThemeSerializer), status: :created }
else else
format.json { render json: @theme.errors, status: :unprocessable_entity } format.json { render json: @theme.errors, status: :unprocessable_entity }
end end
@ -250,7 +250,7 @@ class Admin::ThemesController < Admin::AdminController
log_theme_component_disabled if disables_component log_theme_component_disabled if disables_component
log_theme_component_enabled if enables_component log_theme_component_enabled if enables_component
format.json { render json: @theme, status: :ok } format.json { render json: serialize_data(@theme, ThemeSerializer), status: :ok }
else else
format.json do format.json do
error = @theme.errors.full_messages.join(", ").presence error = @theme.errors.full_messages.join(", ").presence

View File

@ -17,12 +17,18 @@ class Admin::WebHooksController < Admin::AdminController
data = serialize_data(web_hooks, AdminWebHookSerializer, root: "web_hooks") data = serialize_data(web_hooks, AdminWebHookSerializer, root: "web_hooks")
serialized_grouped_event_types =
WebHookEventType.active_grouped.transform_values do |array|
serialize_data(array, WebHookEventTypeSerializer)
end
json = { json = {
web_hooks: data.delete("web_hooks"), web_hooks: data.delete("web_hooks"),
extras: extras:
data.merge( data.merge(
grouped_event_types: WebHookEventType.active_grouped, grouped_event_types: serialized_grouped_event_types,
default_event_types: WebHook.default_event_types, default_event_types:
serialize_data(WebHook.default_event_types, WebHookEventTypeSerializer),
content_types: WebHook.content_types.map { |name, id| { id: id, name: name } }, content_types: WebHook.content_types.map { |name, id| { id: id, name: name } },
delivery_statuses: delivery_statuses:
WebHook.last_delivery_statuses.map { |name, id| { id: id, name: name.to_s } }, WebHook.last_delivery_statuses.map { |name, id| { id: id, name: name.to_s } },

View File

@ -140,7 +140,7 @@ class TopicsController < ApplicationController
custom_message_params: { custom_message_params: {
group: group.name, group: group.name,
}, },
group: group, group: serialize_data(group, BasicGroupSerializer, root: false),
) )
end end

View File

@ -1544,12 +1544,14 @@ class UsersController < ApplicationController
.select(:id, :name, :last_used, :created_at, :method) .select(:id, :name, :last_used, :created_at, :method)
.where(enabled: true) .where(enabled: true)
.order(:created_at) .order(:created_at)
.as_json(only: %i[id name method last_used])
security_keys = security_keys =
current_user current_user
.security_keys .security_keys
.where(factor_type: UserSecurityKey.factor_types[:second_factor]) .where(factor_type: UserSecurityKey.factor_types[:second_factor])
.order(:created_at) .order(:created_at)
.as_json(only: %i[id user_id credential_id public_key factor_type enabled name last_used])
render json: success_json.merge(totps: totp_second_factors, security_keys: security_keys) render json: success_json.merge(totps: totp_second_factors, security_keys: security_keys)
else else

View File

@ -533,7 +533,10 @@ module Jobs
def get_user_archive_fields(user_archive) def get_user_archive_fields(user_archive)
user_archive_array = [] user_archive_array = []
topic_data = user_archive.topic topic_data = user_archive.topic
user_archive = user_archive.as_json user_archive =
user_archive.as_json(
only: %i[topic_id post_number raw cooked like_count reply_count created_at id],
)
topic_data = topic_data =
Topic Topic
.with_deleted .with_deleted

View File

@ -8,8 +8,7 @@ class AdminWebHookSerializer < ApplicationSerializer
:secret, :secret,
:wildcard_web_hook, :wildcard_web_hook,
:verify_certificate, :verify_certificate,
:active, :active
:web_hook_event_types
has_many :categories, serializer: BasicCategorySerializer, embed: :ids, include: true has_many :categories, serializer: BasicCategorySerializer, embed: :ids, include: true
has_many :tags, has_many :tags,
@ -19,10 +18,10 @@ class AdminWebHookSerializer < ApplicationSerializer
embed_key: :name, embed_key: :name,
include: false include: false
has_many :groups, serializer: BasicGroupSerializer, embed: :ids, include: false has_many :groups, serializer: BasicGroupSerializer, embed: :ids, include: false
has_many :web_hook_event_types,
def web_hook_event_types serializer: WebHookEventTypeSerializer,
ActiveModel::ArraySerializer.new(object.web_hook_event_types).as_json root: false,
end embed: :objects
def last_delivery_status def last_delivery_status
object.active ? object.last_delivery_status : WebHook.last_delivery_statuses[:disabled] object.active ? object.last_delivery_status : WebHook.last_delivery_statuses[:disabled]

View File

@ -84,7 +84,10 @@ class SiteSerializer < ApplicationSerializer
end end
def default_dark_color_scheme def default_dark_color_scheme
ColorScheme.find_by_id(SiteSetting.default_dark_mode_color_scheme_id).as_json ColorSchemeSerializer.new(
ColorScheme.find_by_id(SiteSetting.default_dark_mode_color_scheme_id),
root: false,
).as_json
end end
def groups def groups

View File

@ -3,8 +3,7 @@
require "base64" require "base64"
class ThemeSerializer < BasicThemeSerializer class ThemeSerializer < BasicThemeSerializer
attributes :color_scheme, attributes :color_scheme_id,
:color_scheme_id,
:user_selectable, :user_selectable,
:auto_update, :auto_update,
:remote_theme_id, :remote_theme_id,
@ -16,6 +15,7 @@ class ThemeSerializer < BasicThemeSerializer
:disabled_at, :disabled_at,
:theme_fields :theme_fields
has_one :color_scheme, serializer: ColorSchemeSerializer, embed: :object
has_one :user, serializer: UserNameSerializer, embed: :object has_one :user, serializer: UserNameSerializer, embed: :object
has_one :disabled_by, serializer: UserNameSerializer, embed: :object has_one :disabled_by, serializer: UserNameSerializer, embed: :object

View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
class TopicViewBookmarkSerializer < ApplicationSerializer
attributes :id, :bookmarkable_id, :bookmarkable_type, :reminder_at, :name, :auto_delete_preference
end

View File

@ -64,7 +64,6 @@ class TopicViewSerializer < ApplicationSerializer
:is_warning, :is_warning,
:chunk_size, :chunk_size,
:bookmarked, :bookmarked,
:bookmarks,
:message_archived, :message_archived,
:topic_timer, :topic_timer,
:unicode_title, :unicode_title,
@ -85,6 +84,7 @@ class TopicViewSerializer < ApplicationSerializer
has_one :details, serializer: TopicViewDetailsSerializer, root: false, embed: :objects has_one :details, serializer: TopicViewDetailsSerializer, root: false, embed: :objects
has_many :pending_posts, serializer: TopicPendingPostSerializer, root: false, embed: :objects has_many :pending_posts, serializer: TopicPendingPostSerializer, root: false, embed: :objects
has_many :categories, serializer: CategoryBadgeSerializer, embed: :objects has_many :categories, serializer: CategoryBadgeSerializer, embed: :objects
has_many :bookmarks, serializer: TopicViewBookmarkSerializer, root: false, embed: :objects
has_one :published_page, embed: :objects has_one :published_page, embed: :objects
@ -201,10 +201,6 @@ class TopicViewSerializer < ApplicationSerializer
object.has_bookmarks? object.has_bookmarks?
end end
def bookmarks
object.bookmarks
end
def topic_timer def topic_timer
topic_timer = object.topic.public_topic_timer topic_timer = object.topic.public_topic_timer

View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
class WebHookEventTypeSerializer < ApplicationSerializer
attributes :id, :name, :group
end

View File

@ -15,10 +15,10 @@
<a href="<%= path "/login" %>" class='btn btn-primary'><%= SvgSprite.raw_svg('fa-user') %><%= I18n.t('log_in') %></a> <a href="<%= path "/login" %>" class='btn btn-primary'><%= SvgSprite.raw_svg('fa-user') %><%= I18n.t('log_in') %></a>
<%- end %> <%- end %>
<%- if @group&.allow_membership_requests %> <%- if @group&.dig(:allow_membership_requests) %>
<a href="<%= group_path @group.name %>" class='btn btn-primary'><%= SvgSprite.raw_svg('user-plus') %> <%= I18n.t('not_in_group.request_membership') %></a> <a href="<%= group_path @group[:name] %>" class='btn btn-primary'><%= SvgSprite.raw_svg('user-plus') %> <%= I18n.t('not_in_group.request_membership') %></a>
<%- elsif @group&.public_admission %> <%- elsif @group&.dig(:public_admission) %>
<a href="<%= group_path @group.name %>" class='btn btn-primary'><%= SvgSprite.raw_svg('user-plus') %> <%= I18n.t('not_in_group.join_group') %></a> <a href="<%= group_path @group[:name] %>" class='btn btn-primary'><%= SvgSprite.raw_svg('user-plus') %> <%= I18n.t('not_in_group.join_group') %></a>
<%- end %> <%- end %>
</div> </div>

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
module ActiveRecordSerializationSafety
class BlockedSerializationError < StandardError
end
def serializable_hash(options = nil)
if options.nil? || options[:only].nil?
message =
"Serializing ActiveRecord models (#{self.class.name}) without specifying fields is not allowed. Use a Serializer, or pass the :only option to #serializable_hash. More info: https://meta.discourse.org/t/-/314495"
if Rails.env == "production"
Rails.logger.info(message)
else
raise BlockedSerializationError.new(message)
end
end
super
end
end
ActiveRecord::Base.prepend(ActiveRecordSerializationSafety)

View File

@ -113,6 +113,12 @@ module ChatSpecHelpers
end end
def create_draft(channel, thread: nil, user: Discourse.system_user, data: { message: "draft" }) def create_draft(channel, thread: nil, user: Discourse.system_user, data: { message: "draft" })
if data[:uploads]
data[:uploads] = data[:uploads].map do |upload|
UploadSerializer.new(upload, root: false).as_json
end
end
result = result =
::Chat::UpsertDraft.call( ::Chat::UpsertDraft.call(
guardian: user.guardian, guardian: user.guardian,

View File

@ -1410,7 +1410,7 @@ RSpec.describe TopicQuery do
end end
def read(user, topic, post_number) def read(user, topic, post_number)
TopicUser.update_last_read(user, topic, post_number, post_number, 10_000) TopicUser.update_last_read(user, topic.id, post_number, post_number, 10_000)
end end
before do before do

View File

@ -222,7 +222,7 @@ RSpec.describe Admin::ThemesController do
end end
it "can import a theme from Git" do it "can import a theme from Git" do
RemoteTheme.stubs(:import_theme) RemoteTheme.stubs(:import_theme).returns(Fabricate(:theme))
post "/admin/themes/import.json", post "/admin/themes/import.json",
params: { params: {
remote: " https://github.com/discourse/discourse-brand-header.git ", remote: " https://github.com/discourse/discourse-brand-header.git ",
@ -1076,12 +1076,11 @@ RSpec.describe Admin::ThemesController do
end end
it "should return the right error when value used to update a theme setting of `objects` typed is invalid" do it "should return the right error when value used to update a theme setting of `objects` typed is invalid" do
field = theme.set_field(
theme.set_field( target: :settings,
target: :settings, name: "yaml",
name: "yaml", value: File.read("#{Rails.root}/spec/fixtures/theme_settings/objects_settings.yaml"),
value: File.read("#{Rails.root}/spec/fixtures/theme_settings/objects_settings.yaml"), )
)
theme.save! theme.save!
@ -1101,12 +1100,11 @@ RSpec.describe Admin::ThemesController do
end end
it "should be able to update a theme setting of `objects` typed" do it "should be able to update a theme setting of `objects` typed" do
field = theme.set_field(
theme.set_field( target: :settings,
target: :settings, name: "yaml",
name: "yaml", value: File.read("#{Rails.root}/spec/fixtures/theme_settings/objects_settings.yaml"),
value: File.read("#{Rails.root}/spec/fixtures/theme_settings/objects_settings.yaml"), )
)
theme.save! theme.save!
@ -1354,7 +1352,7 @@ RSpec.describe Admin::ThemesController do
let(:theme_setting) do let(:theme_setting) do
yaml = File.read("#{Rails.root}/spec/fixtures/theme_settings/objects_settings.yaml") yaml = File.read("#{Rails.root}/spec/fixtures/theme_settings/objects_settings.yaml")
field = theme.set_field(target: :settings, name: "yaml", value: yaml) theme.set_field(target: :settings, name: "yaml", value: yaml)
theme.save! theme.save!
theme.settings theme.settings
end end

View File

@ -108,6 +108,11 @@ RSpec.describe "notifications" do
response "200", "notifications marked read" do response "200", "notifications marked read" do
schema type: :object, properties: { success: { type: :string } } schema type: :object, properties: { success: { type: :string } }
let(:notification) do
notification = Fabricate(:notification)
NotificationSerializer.new(notification).as_json
end
run_test! run_test!
end end
end end

View File

@ -221,7 +221,10 @@ RSpec.describe "posts" do
expected_response_schema = load_spec_schema("topic_create_response") expected_response_schema = load_spec_schema("topic_create_response")
schema expected_response_schema schema expected_response_schema
let(:params) { Fabricate(:post) } let(:params) do
post = Fabricate(:post)
post.serializable_hash(only: %i[topic_id raw created_at]).as_json
end
it_behaves_like "a JSON endpoint", 200 do it_behaves_like "a JSON endpoint", 200 do
let(:expected_response_schema) { expected_response_schema } let(:expected_response_schema) { expected_response_schema }

View File

@ -113,7 +113,7 @@ RSpec.describe SiteSerializer do
scheme = ColorScheme.last scheme = ColorScheme.last
SiteSetting.default_dark_mode_color_scheme_id = scheme.id SiteSetting.default_dark_mode_color_scheme_id = scheme.id
serialized = described_class.new(Site.new(guardian), scope: guardian, root: false).as_json serialized = described_class.new(Site.new(guardian), scope: guardian, root: false).as_json
default_dark_scheme = expect(serialized[:default_dark_color_scheme]["name"]).to eq(scheme.name) default_dark_scheme = expect(serialized[:default_dark_color_scheme][:name]).to eq(scheme.name)
SiteSetting.default_dark_mode_color_scheme_id = -1 SiteSetting.default_dark_mode_color_scheme_id = -1
serialized = described_class.new(Site.new(guardian), scope: guardian, root: false).as_json serialized = described_class.new(Site.new(guardian), scope: guardian, root: false).as_json