Repository: scrapy/scrapy

Issue:
Whitelist form methods in FormRequest.from_response
Hi team, according to this [article](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-method), there are 3 methods that are accepted in form's `method` attribute. I can't remember about other methods, but I'd agree to consider the rest of [REST verbs](https://restfulapi.net/http-methods/) if that's common. Anyway, I think it should be a good idea to whitelist the accepted methods to avoid scenarios like this [vulnerability exploitation](https://medium.com/alertot/web-scraping-considered-dangerous-exploiting-the-telnet-service-in-scrapy-1-5-2-ad5260fea0db) that took advantage of form's method.

This issue affects `FormRequest.from_response` and the affected line is: https://github.com/scrapy/scrapy/blob/a3d38041e230f886d55569f861a11a96ab8a1913/scrapy/http/request/form.py#L51

I want to know if there some reason behind this behavior, otherwise I could send a pull request.


Discussion:
Hi @csalazar, well done on the telnet security issue article you published, I loved the attack vector explanation.

You demonstrated clearly how Form's method can be exploited on code injection, so no doubts we would like to restrict the set of valid methods. My suggestion would be to mimic what major browsers are accepting. 


Resolving patch:
diff --git a/scrapy/http/request/form.py b/scrapy/http/request/form.py
--- a/scrapy/http/request/form.py
+++ b/scrapy/http/request/form.py
@@ -18,6 +18,7 @@
 
 
 class FormRequest(Request):
+    valid_form_methods = ['GET', 'POST']
 
     def __init__(self, *args, **kwargs):
         formdata = kwargs.pop('formdata', None)
@@ -48,7 +49,13 @@ def from_response(cls, response, formname=None, formid=None, formnumber=0, formd
         form = _get_form(response, formname, formid, formnumber, formxpath)
         formdata = _get_inputs(form, formdata, dont_click, clickdata, response)
         url = _get_form_url(form, kwargs.pop('url', None))
+
         method = kwargs.pop('method', form.method)
+        if method is not None:
+            method = method.upper()
+            if method not in cls.valid_form_methods:
+                method = 'GET'
+
         return cls(url=url, method=method, formdata=formdata, **kwargs)
 
 


Test patch:
diff --git a/tests/test_http_request.py b/tests/test_http_request.py
--- a/tests/test_http_request.py
+++ b/tests/test_http_request.py
@@ -1100,6 +1100,20 @@ def test_from_response_css(self):
         self.assertRaises(ValueError, self.request_class.from_response,
                           response, formcss="input[name='abc']")
 
+    def test_from_response_valid_form_methods(self):
+        body = """<form action="post.php" method="%s">
+            <input type="hidden" name="one" value="1">
+            </form>"""
+
+        for method in self.request_class.valid_form_methods:
+            response = _buildresponse(body % method)
+            r = self.request_class.from_response(response)
+            self.assertEqual(r.method, method)
+
+        response = _buildresponse(body % 'UNKNOWN')
+        r = self.request_class.from_response(response)
+        self.assertEqual(r.method, 'GET')
+
 
 def _buildresponse(body, **kwargs):
     kwargs.setdefault('body', body)