You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
docuseal/lib/account_logo.rb

64 lines
2.0 KiB

# frozen_string_literal: true
# AccountLogo handles validation/sanitization of uploads before they hit
# ActiveStorage. Raster images pass through unchanged. SVGs are scrubbed of
# scripts, event-handler attributes, foreign-object elements, and external
# resource references — the standard XSS surface for inline-embedded SVG.
module AccountLogo
Sanitized = Struct.new(:io, :filename, :content_type)
# Allowed top-level/nested SVG-related element + attribute names that
# carry external resource URIs. We don't enumerate every safe attribute —
# we instead drop the known-dangerous ones by name pattern.
EVENT_HANDLER_PREFIX = 'on'
EXTERNAL_REF_ATTRS = %w[href xlink:href].freeze
module_function
def sanitize_upload(uploaded_file)
content_type = uploaded_file.content_type.to_s
if content_type == 'image/svg+xml'
bytes = uploaded_file.read
uploaded_file.rewind if uploaded_file.respond_to?(:rewind)
cleaned = sanitize_svg(bytes)
Sanitized.new(StringIO.new(cleaned), uploaded_file.original_filename.to_s, 'image/svg+xml')
else
io = uploaded_file.respond_to?(:tempfile) ? uploaded_file.tempfile : uploaded_file
Sanitized.new(io, uploaded_file.original_filename.to_s, content_type)
end
end
# Public for spec testing.
def sanitize_svg(svg_string)
doc = Nokogiri::XML(svg_string) { |c| c.nonet.noblanks }
doc.traverse do |node|
next unless node.element?
local = node.name.to_s.downcase.sub(/.*:/, '')
if local == 'script' || local == 'foreignobject'
node.remove
next
end
node.attributes.each_value do |attr|
name = attr.name.to_s
downcased = name.downcase
if downcased.start_with?(EVENT_HANDLER_PREFIX)
node.remove_attribute(name)
next
end
next unless EXTERNAL_REF_ATTRS.include?(downcased) || downcased.end_with?(':href')
value = attr.value.to_s.strip
node.remove_attribute(name) unless value.start_with?('#', 'data:')
end
end
doc.to_xml
end
end