このコミットが含まれているのは:
2026-07-18 02:08:20 +09:00
コミット 9552081133
9個のファイルの変更351行の追加151行の削除
+65 -8
ファイルの表示
@@ -3,6 +3,7 @@ class Post < ApplicationRecord
require 'mini_magick'
require 'nokogiri'
require 'stringio'
require 'timeout'
class RemoteThumbnailFetchFailed < StandardError; end
@@ -15,6 +16,7 @@ class Post < ApplicationRecord
REMOTE_SVG_CONTENT_TYPE = 'image/svg+xml'.freeze
MAX_SVG_DIMENSION = 4_096
MAX_SVG_PIXELS = 16_777_216
THUMBNAIL_PROCESS_TIMEOUT = 5.seconds
def self.resized_thumbnail_attachment(upload, content_type: nil)
upload.rewind
@@ -216,15 +218,37 @@ class Post < ApplicationRecord
end
def self.image_for_thumbnail_upload(bytes, content_type: nil)
return MiniMagick::Image.read(bytes) unless svg_content_type?(content_type)
return decode_raster_thumbnail(bytes) unless svg_content_type?(content_type) || svg_document_bytes?(bytes)
MiniMagick::Image.read(sanitised_svg_bytes(bytes))
decode_svg_thumbnail(bytes)
end
def self.svg_content_type?(content_type)
content_type.to_s.split(';', 2).first.to_s.downcase.strip == REMOTE_SVG_CONTENT_TYPE
end
def self.svg_document_bytes?(bytes)
document = Nokogiri::XML(
bytes,
nil,
nil,
Nokogiri::XML::ParseOptions::STRICT |
Nokogiri::XML::ParseOptions::NONET)
document.root&.name == 'svg'
rescue Nokogiri::XML::SyntaxError
false
end
def self.decode_raster_thumbnail(bytes)
Timeout.timeout(THUMBNAIL_PROCESS_TIMEOUT) { MiniMagick::Image.read(bytes) }
end
def self.decode_svg_thumbnail(bytes)
Timeout.timeout(THUMBNAIL_PROCESS_TIMEOUT) do
MiniMagick::Image.read(sanitised_svg_bytes(bytes))
end
end
def self.sanitised_svg_bytes(bytes)
parse_options =
Nokogiri::XML::ParseOptions::STRICT |
@@ -256,20 +280,44 @@ class Post < ApplicationRecord
name = node.name.to_s.downcase
next true if name == 'script' || name == 'foreignobject'
next style_contains_disallowed_urls?(node.text.to_s) if name == 'style'
node.attribute_nodes.any? do |attribute|
attribute_name = attribute.name.to_s.downcase
attribute_value = attribute.value.to_s
attribute_name.start_with?('on') ||
attribute_name == 'href' ||
attribute_name == 'xlink:href' ||
attribute_name == 'src' ||
(attribute_name == 'style' && attribute_value.match?(/url\s*\(/i)) ||
attribute_value.match?(/\A\s*(?:https?:|data:|\/\/)/i)
external_svg_reference?(attribute_name, attribute_value)
end
end
end
def self.external_svg_reference?(attribute_name, attribute_value)
return external_svg_url?(attribute_value) if ['href', 'xlink:href', 'src'].include?(attribute_name)
return style_contains_disallowed_urls?(attribute_value) if attribute_name == 'style'
return svg_url_function_disallowed?(attribute_value) if attribute_value.match?(/url\s*\(/i)
false
end
def self.external_svg_url?(value)
stripped = value.to_s.strip
return false if stripped.blank? || stripped.start_with?('#')
stripped.match?(/\A(?:https?:|data:|\/\/)/i)
end
def self.style_contains_disallowed_urls?(value)
text = value.to_s
text.match?(/@import/i) || svg_url_function_disallowed?(text)
end
def self.svg_url_function_disallowed?(value)
value.to_s.scan(/url\s*\(([^)]*)\)/i).flatten.any? do |entry|
reference = entry.to_s.strip.delete_prefix("'").delete_prefix('"').delete_suffix("'").delete_suffix('"')
reference.present? && !(reference.start_with?('#'))
end
end
def self.svg_dimensions(root)
width = svg_length_to_pixels(root['width'])
height = svg_length_to_pixels(root['height'])
@@ -287,15 +335,24 @@ class Post < ApplicationRecord
matched = /\A([0-9]+(?:\.[0-9]+)?)(px)?\z/i.match(value.to_s.strip)
return nil if matched == nil
Float(matched[1])
pixels = Float(matched[1])
return nil unless pixels.finite? && pixels.positive?
pixels
rescue ArgumentError
nil
end
private_class_method :image_for_thumbnail_upload,
:svg_content_type?,
:decode_raster_thumbnail,
:decode_svg_thumbnail,
:sanitised_svg_bytes,
:svg_uses_disallowed_features?,
:external_svg_reference?,
:external_svg_url?,
:style_contains_disallowed_urls?,
:svg_url_function_disallowed?,
:svg_dimensions,
:svg_length_to_pixels