CVE-2021-4162 (published 2021-12-25T12:15Z)

archivy is vulnerable to Cross-Site Request Forgery (CSRF)

Severity: MEDIUM
CVSS v2 base score: 4.3 (AV:N/AC:M/Au:N/C:N/I:P/A:N)
CVSS v3 base score: 4.3, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N)

Weakness information:
cwe_id: CWE-352
cwe_name: Cross-Site Request Forgery (CSRF)
description: The web application does not, or can not, sufficiently verify whether a well-formed, valid, consistent request was intentionally provided by the user who submitted the request.
extended_description: When a web server is designed to receive a request from a client without any mechanism for verifying that it was intentionally sent, then it might be possible for an attacker to trick a client into making an unintentional request to the web server which will be treated as an authentic request. This can be done via a URL, image load, XMLHttpRequest, etc. and can result in exposure of data or unintended code execution.
         

Fix information:
repo_url: https://github.com/archivy/archivy
commit_details: repo_url: https://github.com/archivy/archivy
author: Uzay-G
author_date: 2021-12-24 12:16:44+01:00
msg: better CSRF protection; change delete route to POST
repository: repo_url: https://github.com/archivy/archivy
repo_name: archivy/archivy
description: Archivy is a self-hostable knowledge repository that allows you to learn and retain information in your own personal and extensible wiki.
repo_language: Python
owner: archivy
file_changes: filename: __init__.py
change_type: ModificationType.MODIFY
diff: @@ -6,6 +6,7 @@ from elasticsearch.exceptions import RequestError
 from flask import Flask
 from flask_compress import Compress
 from flask_login import LoginManager
+from flask_wtf.csrf import CSRFProtect
 
 from archivy import helpers
 from archivy.api import api_bp
@@ -77,6 +78,7 @@ login_manager = LoginManager()
 login_manager.login_view = "login"
 login_manager.init_app(app)
 app.register_blueprint(api_bp, url_prefix="/api")
+csrf = CSRFProtect(app)
 
 # compress files
 Compress(app)

programming_language: Python
filename: cmd_exec.py
change_type: ModificationType.MODIFY
diff: @@ -137,10 +137,8 @@ def _get_download_link(field_info):
 
 class RequestToCommandArgs:
     def __init__(self):
-        field_infos = [
-            FieldInfo.factory(key)
-            for key in list(request.form.keys()) + list(request.files.keys())
-        ]
+        keys = [key for key in list(request.form.keys()) + list(request.files.keys())]
+        field_infos = [FieldInfo.factory(key) for key in keys if key != "csrf_token"]
         # important to sort them so they will be in expected order on command line
         self.field_infos = list(sorted(field_infos))
 

programming_language: Python
filename: routes.py
change_type: ModificationType.MODIFY
diff: @@ -232,7 +232,7 @@ def move_item(dataobj_id):
         return redirect(f"/dataobj/{dataobj_id}")
 
 
-@app.route("/dataobj/delete/<int:dataobj_id>", methods=["DELETE", "GET"])
+@app.route("/dataobj/delete/<int:dataobj_id>", methods=["POST"])
 def delete_data(dataobj_id):
     try:
         data.delete_item(dataobj_id)

programming_language: Python
filename: command_form.html
change_type: ModificationType.MODIFY
diff: @@ -29,6 +29,7 @@
             </div>
         {% endfor %}
     {% endfor %}
+	<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
     <button type="submit" id="submit_btn" class="btn btn-primary m-2">Run</button>
 </form>
 

programming_language: HTML
filename: show.html
change_type: ModificationType.MODIFY
diff: @@ -71,7 +71,7 @@
             <svg class="octicon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M11.013 1.427a1.75 1.75 0 012.474 0l1.086 1.086a1.75 1.75 0 010 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 01-.927-.928l.929-3.25a1.75 1.75 0 01.445-.758l8.61-8.61zm1.414 1.06a.25.25 0 00-.354 0L10.811 3.75l1.439 1.44 1.263-1.263a.25.25 0 000-.354l-1.086-1.086zM11.189 6.25L9.75 4.81l-6.286 6.287a.25.25 0 00-.064.108l-.558 1.953 1.953-.558a.249.249 0 00.108-.064l6.286-6.286z"></path></svg>
             <span>Edit</span>
           </button>
-          <form action="/dataobj/delete/{{ dataobj['id'] }}" method="delete" onsubmit="return confirm('Delete this item permanently?')" novalidate>
+          <form action="/dataobj/delete/{{ dataobj['id'] }}" method="POST" onsubmit="return confirm('Delete this item permanently?')" novalidate>
             {{ form.hidden_tag() }}
             <button class="btn btn-delete">
               <svg class="octicon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M6.5 1.75a.25.25 0 01.25-.25h2.5a.25.25 0 01.25.25V3h-3V1.75zm4.5 0V3h2.25a.75.75 0 010 1.5H2.75a.75.75 0 010-1.5H5V1.75C5 .784 5.784 0 6.75 0h2.5C10.216 0 11 .784 11 1.75zM4.496 6.675a.75.75 0 10-1.492.15l.66 6.6A1.75 1.75 0 005.405 15h5.19c.9 0 1.652-.681 1.741-1.576l.66-6.6a.75.75 0 00-1.492-.149l-.66 6.6a.25.25 0 01-.249.225h-5.19a.25.25 0 01-.249-.225l-.66-6.6z"></path></svg>

programming_language: HTML
filename: test_routes.py
change_type: ModificationType.MODIFY
diff: @@ -61,12 +61,12 @@ def test_get_dataobj(test_app, client: FlaskClient, note_fixture):
 
 
 def test_get_delete_dataobj_not_found(test_app, client: FlaskClient):
-    response = client.get("/dataobj/delete/1")
+    response = client.post("/dataobj/delete/1")
     assert response.status_code == 302
 
 
 def test_get_delete_dataobj(test_app, client: FlaskClient, note_fixture):
-    response = client.get("/dataobj/delete/1")
+    response = client.post("/dataobj/delete/1")
     assert response.status_code == 302
 
 

programming_language: Python