SECURITY: Add content-disposition: attachment for SVG uploads

* strip out the href and xlink:href attributes from use element that
  are _not_ anchors in svgs which can be used for XSS
* adding the content-disposition: attachment ensures that
  uploaded SVGs cannot be opened and executed using the XSS exploit.
  svgs embedded using an img tag do not suffer from the same exploit
This commit is contained in:
Martin Brennan 2020-07-09 13:31:48 +10:00
parent 271d6319ce
commit 3f7658cc6e
No known key found for this signature in database
GPG Key ID: A08063EEF3EA26A4
6 changed files with 37 additions and 7 deletions

View File

@ -249,7 +249,7 @@ class UploadsController < ApplicationController
content_type: MiniMime.lookup_by_filename(upload.original_filename)&.content_type
}
if !FileHelper.is_supported_image?(upload.original_filename)
if !FileHelper.is_inline_image?(upload.original_filename)
opts[:disposition] = "attachment"
elsif params[:inline]
opts[:disposition] = "inline"

View File

@ -185,9 +185,12 @@ server {
try_files $uri =404;
}
# this allows us to bypass rails
location ~* \.(gif|png|jpg|jpeg|bmp|tif|tiff|svg|ico|webp)$ {
location ~* \.(gif|png|jpg|jpeg|bmp|tif|tiff|ico|webp)$ {
try_files $uri =404;
}
# SVG needs an extra header attached
location ~* \.(svg)$ {
}
# thumbnails & optimized images
location ~ /_?optimized/ {
try_files $uri =404;

View File

@ -17,6 +17,10 @@ class FileHelper
(filename =~ supported_images_regexp).present?
end
def self.is_inline_image?(filename)
(filename =~ inline_images_regexp).present?
end
def self.is_supported_media?(filename)
(filename =~ supported_media_regexp).present?
end
@ -136,6 +140,11 @@ class FileHelper
@@supported_images ||= Set.new %w{jpg jpeg png gif svg ico webp}
end
def self.inline_images
# SVG cannot safely be shown as a document
@@inline_images ||= supported_images - %w{svg}
end
def self.supported_audio
@@supported_audio ||= Set.new %w{mp3 ogg wav m4a}
end
@ -148,6 +157,10 @@ class FileHelper
@@supported_images_regexp ||= /\.(#{supported_images.to_a.join("|")})$/i
end
def self.inline_images_regexp
@@inline_images_regexp ||= /\.(#{inline_images.to_a.join("|")})$/i
end
def self.supported_media_regexp
media = supported_images | supported_audio | supported_video
@@supported_media_regexp ||= /\.(#{media.to_a.join("|")})$/i

View File

@ -54,11 +54,12 @@ module FileStore
content_type: opts[:content_type].presence || MiniMime.lookup_by_filename(filename)&.content_type
}
# add a "content disposition: attachment" header with the original filename
# for everything but images. audio and video will still stream correctly in
# HTML players, and when a direct link is provided to any file but an image
# it will download correctly in the browser.
if !FileHelper.is_supported_image?(filename)
# add a "content disposition: attachment" header with the original
# filename for everything but safe images (not SVG). audio and video will
# still stream correctly in HTML players, and when a direct link is
# provided to any file but an image it will download correctly in the
# browser.
if !FileHelper.is_inline_image?(filename)
options[:content_disposition] = ActionDispatch::Http::ContentDisposition.format(
disposition: "attachment", filename: filename
)

View File

@ -232,6 +232,11 @@ module FileStore
if upload&.secure
options[:acl] = "private"
end
elsif !FileHelper.is_inline_image?(name)
upload = Upload.find_by(url: "/#{file}")
options[:content_disposition] = ActionDispatch::Http::ContentDisposition.format(
disposition: "attachment", filename: upload&.original_filename || name
)
end
etag ||= Digest::MD5.file(path).hexdigest

View File

@ -278,6 +278,14 @@ class UploadCreator
doc = Nokogiri::XML(@file)
doc.xpath(svg_whitelist_xpath).remove
doc.xpath("//@*[starts-with(name(), 'on')]").remove
doc.css('use').each do |use_el|
if use_el.attr('href')
use_el.remove_attribute('href') unless use_el.attr('href').starts_with?('#')
end
if use_el.attr('xlink:href')
use_el.remove_attribute('xlink:href') unless use_el.attr('xlink:href').starts_with?('#')
end
end
File.write(@file.path, doc.to_s)
@file.rewind
end