diff --git a/lib/sinatra/base.rb b/lib/sinatra/base.rb
index d7e2d65ad9..cbb3f2fd7b 100644
--- a/lib/sinatra/base.rb
+++ b/lib/sinatra/base.rb
@@ -396,13 +396,20 @@ def content_type(type = nil, params = {})
response['Content-Type'] = mime_type
end
+ # https://html.spec.whatwg.org/#multipart-form-data
+ MULTIPART_FORM_DATA_REPLACEMENT_TABLE = {
+ '"' => '%22',
+ "\r" => '%0D',
+ "\n" => '%0A'
+ }.freeze
+
# Set the Content-Disposition to "attachment" with the specified filename,
# instructing the user agents to prompt to save.
def attachment(filename = nil, disposition = :attachment)
response['Content-Disposition'] = disposition.to_s.dup
return unless filename
- params = format('; filename="%s"', File.basename(filename))
+ params = format('; filename="%s"', File.basename(filename).gsub(/["\r\n]/, MULTIPART_FORM_DATA_REPLACEMENT_TABLE))
response['Content-Disposition'] << params
ext = File.extname(filename)
content_type(ext) unless response['Content-Type'] || ext.empty?
diff --git a/test/helpers_test.rb b/test/helpers_test.rb
index 67dffce1fb..71b42a8fd3 100644
--- a/test/helpers_test.rb
+++ b/test/helpers_test.rb
@@ -781,6 +781,18 @@ def attachment_app(filename=nil)
assert_equal '', body
end
+ it 'escapes filename in the Content-Disposition header according to the multipart form data spec in WHATWG living standard' do
+ mock_app do
+ get('/attachment') do
+ attachment "test.xml\";\r\next=.txt"
+ response.write("")
+ end
+ end
+
+ get '/attachment'
+ assert_equal 'attachment; filename="test.xml%22;%0D%0Aext=.txt"', response['Content-Disposition']
+ assert_equal '', body
+ end
end
describe 'send_file' do