CVE-2021-4164 (Python, published 2022-01-17T13:15Z)
Description: value: calibre-web is vulnerable to Cross-Site Request Forgery (CSRF)
Severity: MEDIUM (CVSS v3 base score: 8.8)
Weakness: CWE-352 \u2014 Cross-Site Request Forgery (CSRF)
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.
Repository: https://github.com/janeczku/calibre-web
Files changed:
cps/admin.py
cps/editbooks.py
cps/kobo_auth.py
cps/shelf.py
cps/static/js/main.js
cps/templates/shelf.html
cps/web.py
Commit message: Migrated some routes to POST
- delete shelf, import ldap users
- delete_kobo token, kobo force full sync
- shutdown, reconnect, shutdown


Vulnerable code:
@admi.route("/shutdown")
    task = int(request.args.get("parameter").strip())
@admi.route("/ajax/fullsync")
@admi.route("/admin/resetpassword/<int:user_id>")
@admi.route('/import_ldap_users')
try:
    from functools import wraps
except ImportError:
    pass  # We're not using Python 3



@editbook.route("/ajax/delete/<int:book_id>")
@editbook.route("/delete/<int:book_id>", defaults={'book_format': ""})
@editbook.route("/delete/<int:book_id>/<string:book_format>")
@editbook.route("/upload", methods=["GET", "POST"])
try:
    from functools import wraps
except ImportError:
    pass  # We're not using Python 3

@kobo_auth.route("/deleteauthtoken/<int:user_id>")
@shelf.route("/shelf/add/<int:shelf_id>/<int:book_id>")
@shelf.route("/shelf/massadd/<int:shelf_id>")
@shelf.route("/shelf/remove/<int:shelf_id>/<int:book_id>")
@shelf.route("/shelf/delete/<int:shelf_id>")
                method:"get",
            url: window.location.pathname + "/../../shutdown",
            data: {"parameter":0},
            url: window.location.pathname + "/../../shutdown",
            data: {"parameter":1},
            data: {"parameter":2},
                    method: "get",
                    method:"get",
                } else {                	
                window.location.href = window.location.pathname + "/../../shelf/delete/" + value
            method:"get",
      <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
      <div class="btn btn-danger" id="delete_shelf" data-value="{{ shelf.id }}">{{ _('Delete this Shelf') }}</div>
<!--div id="DeleteShelfDialog" class="modal fade" role="dialog">
  <div class="modal-dialog modal-sm">
    <div class="modal-content">
      <div class="modal-header bg-danger text-center">
      <span>{{_('Are you sure you want to delete this shelf?')}}</span>
      </div>
      <div class="modal-body text-center">
        <span>{{_('Shelf will be deleted for all users')}}</span>
          <p></p>
        <a id="confirm" href="{{ url_for('shelf.delete_shelf', shelf_id=shelf.id) }}" class="btn btn-danger">{{_('OK')}}</a>
        <button type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
      </div>
    </div>
  </div>
</div-->

@app.route("/reconnect")
@web.route('/send/<int:book_id>/<book_format>/<int:convert>')

Fixed code:
@admi.route("/shutdown", methods=["POST"])
    task = request.get_json().get('parameter', -1)
@admi.route("/ajax/fullsync", methods=["POST"])
@admi.route("/admin/resetpassword/<int:user_id>", methods=["POST"])
@admi.route('/import_ldap_users', methods=["POST"])
from functools import wraps

@editbook.route("/ajax/delete/<int:book_id>", methods=["POST"])
@editbook.route("/delete/<int:book_id>", defaults={'book_format': ""}, methods=["POST"])
@editbook.route("/delete/<int:book_id>/<string:book_format>", methods=["POST"])
@editbook.route("/upload", methods=["POST"])
from functools import wraps
@kobo_auth.route("/deleteauthtoken/<int:user_id>", methods=["POST"])
@shelf.route("/shelf/add/<int:shelf_id>/<int:book_id>", methods=["POST"])
@shelf.route("/shelf/massadd/<int:shelf_id>", methods=["POST"])
@shelf.route("/shelf/remove/<int:shelf_id>/<int:book_id>", methods=["POST"])
@shelf.route("/shelf/delete/<int:shelf_id>", methods=["POST"])
        flash(_("Shelf successfully deleted"), category="success")
                method:"post",
            method:"post",
            contentType: "application/json; charset=utf-8",
            url: getPath() + "/shutdown",
            data: JSON.stringify({"parameter":0}),
            method:"post",
            contentType: "application/json; charset=utf-8",
            url: getPath() + "/shutdown",
            data: JSON.stringify({"parameter":1}),
            method:"post",
            contentType: "application/json; charset=utf-8",
            data: JSON.stringify({"parameter":2}),
                    method: "post",
                    method:"post",
                } else {
                $("#delete_shelf").closest("form").submit()
            method:"post",
            contentType: "application/json; charset=utf-8",
      <form action="{{url_for('shelf.delete_shelf', shelf_id=shelf.id)}}" method="post">
        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
        <div class="btn btn-danger" id="delete_shelf" data-value="{{ shelf.id }}">{{ _('Delete this Shelf') }}</div>
      </form>
# method is available without login and not protected by CSRF to make it easy reachable
@app.route("/reconnect", methods=['GET'])
@web.route('/send/<int:book_id>/<book_format>/<int:convert>', methods=["POST"])