mirror of https://github.com/docusealco/docuseal
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.
66 lines
2.1 KiB
66 lines
2.1 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
|
|
|
|
if EXTERNAL_REF_ATTRS.include?(downcased) || downcased.end_with?(':href')
|
|
value = attr.value.to_s.strip
|
|
unless value.start_with?('#') || value.start_with?('data:')
|
|
node.remove_attribute(name)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
doc.to_xml
|
|
end
|
|
end
|