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:
parent
fd38c2fac3
commit
31e31ef449
|
@ -252,7 +252,7 @@ class UploadsController < ApplicationController
|
||||||
content_type: MiniMime.lookup_by_filename(upload.original_filename)&.content_type
|
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"
|
opts[:disposition] = "attachment"
|
||||||
elsif params[:inline]
|
elsif params[:inline]
|
||||||
opts[:disposition] = "inline"
|
opts[:disposition] = "inline"
|
||||||
|
|
|
@ -185,9 +185,12 @@ server {
|
||||||
try_files $uri =404;
|
try_files $uri =404;
|
||||||
}
|
}
|
||||||
# this allows us to bypass rails
|
# 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;
|
try_files $uri =404;
|
||||||
}
|
}
|
||||||
|
# SVG needs an extra header attached
|
||||||
|
location ~* \.(svg)$ {
|
||||||
|
}
|
||||||
# thumbnails & optimized images
|
# thumbnails & optimized images
|
||||||
location ~ /_?optimized/ {
|
location ~ /_?optimized/ {
|
||||||
try_files $uri =404;
|
try_files $uri =404;
|
||||||
|
|
|
@ -17,6 +17,10 @@ class FileHelper
|
||||||
(filename =~ supported_images_regexp).present?
|
(filename =~ supported_images_regexp).present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.is_inline_image?(filename)
|
||||||
|
(filename =~ inline_images_regexp).present?
|
||||||
|
end
|
||||||
|
|
||||||
def self.is_supported_media?(filename)
|
def self.is_supported_media?(filename)
|
||||||
(filename =~ supported_media_regexp).present?
|
(filename =~ supported_media_regexp).present?
|
||||||
end
|
end
|
||||||
|
@ -136,6 +140,11 @@ class FileHelper
|
||||||
@@supported_images ||= Set.new %w{jpg jpeg png gif svg ico webp}
|
@@supported_images ||= Set.new %w{jpg jpeg png gif svg ico webp}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.inline_images
|
||||||
|
# SVG cannot safely be shown as a document
|
||||||
|
@@inline_images ||= supported_images - %w{svg}
|
||||||
|
end
|
||||||
|
|
||||||
def self.supported_audio
|
def self.supported_audio
|
||||||
@@supported_audio ||= Set.new %w{mp3 ogg wav m4a}
|
@@supported_audio ||= Set.new %w{mp3 ogg wav m4a}
|
||||||
end
|
end
|
||||||
|
@ -148,6 +157,10 @@ class FileHelper
|
||||||
@@supported_images_regexp ||= /\.(#{supported_images.to_a.join("|")})$/i
|
@@supported_images_regexp ||= /\.(#{supported_images.to_a.join("|")})$/i
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.inline_images_regexp
|
||||||
|
@@inline_images_regexp ||= /\.(#{inline_images.to_a.join("|")})$/i
|
||||||
|
end
|
||||||
|
|
||||||
def self.supported_media_regexp
|
def self.supported_media_regexp
|
||||||
media = supported_images | supported_audio | supported_video
|
media = supported_images | supported_audio | supported_video
|
||||||
@@supported_media_regexp ||= /\.(#{media.to_a.join("|")})$/i
|
@@supported_media_regexp ||= /\.(#{media.to_a.join("|")})$/i
|
||||||
|
|
|
@ -54,11 +54,12 @@ module FileStore
|
||||||
content_type: opts[:content_type].presence || MiniMime.lookup_by_filename(filename)&.content_type
|
content_type: opts[:content_type].presence || MiniMime.lookup_by_filename(filename)&.content_type
|
||||||
}
|
}
|
||||||
|
|
||||||
# add a "content disposition: attachment" header with the original filename
|
# add a "content disposition: attachment" header with the original
|
||||||
# for everything but images. audio and video will still stream correctly in
|
# filename for everything but safe images (not SVG). audio and video will
|
||||||
# HTML players, and when a direct link is provided to any file but an image
|
# still stream correctly in HTML players, and when a direct link is
|
||||||
# it will download correctly in the browser.
|
# provided to any file but an image it will download correctly in the
|
||||||
if !FileHelper.is_supported_image?(filename)
|
# browser.
|
||||||
|
if !FileHelper.is_inline_image?(filename)
|
||||||
options[:content_disposition] = ActionDispatch::Http::ContentDisposition.format(
|
options[:content_disposition] = ActionDispatch::Http::ContentDisposition.format(
|
||||||
disposition: "attachment", filename: filename
|
disposition: "attachment", filename: filename
|
||||||
)
|
)
|
||||||
|
|
|
@ -232,6 +232,11 @@ module FileStore
|
||||||
if upload&.secure
|
if upload&.secure
|
||||||
options[:acl] = "private"
|
options[:acl] = "private"
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
etag ||= Digest::MD5.file(path).hexdigest
|
etag ||= Digest::MD5.file(path).hexdigest
|
||||||
|
|
|
@ -278,6 +278,14 @@ class UploadCreator
|
||||||
doc = Nokogiri::XML(@file)
|
doc = Nokogiri::XML(@file)
|
||||||
doc.xpath(svg_whitelist_xpath).remove
|
doc.xpath(svg_whitelist_xpath).remove
|
||||||
doc.xpath("//@*[starts-with(name(), 'on')]").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.write(@file.path, doc.to_s)
|
||||||
@file.rewind
|
@file.rewind
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue