From 1808bcdf3424eab0c659ef2d0e85579aab977a1a Mon Sep 17 00:00:00 2001
From: namusyaka <namusyaka@gmail.com>
Date: Wed, 23 Nov 2022 22:24:02 +0900
Subject: [PATCH] escape filename in the Content-Disposition header

According the multipart form data spec in WHATWG living standard.
Ref: https://html.spec.whatwg.org/#multipart-form-data

Origin:
https://github.com/sinatra/sinatra/commit/1808bcdf3424eab0c659ef2d0e85579aab977a1a
---
 lib/sinatra/base.rb | 19 +++++++++++++------
 1 file changed, 13 insertions(+), 6 deletions(-)

diff --git a/lib/sinatra/base.rb b/lib/sinatra/base.rb
index f5d7729..b20a1f7 100644
--- a/lib/sinatra/base.rb
+++ b/lib/sinatra/base.rb
@@ -357,16 +357,23 @@ module Sinatra
       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
-      if filename
-        params = '; filename="%s"' % File.basename(filename)
-        response['Content-Disposition'] << params
-        ext = File.extname(filename)
-        content_type(ext) unless response['Content-Type'] or ext.empty?
-      end
+      return unless 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?
     end
 
     # Use the contents of the file at +path+ as the response body.
-- 
2.47.0