CVE-2021-25965 (HTML, published 2021-11-16T10:15Z)
Description: value: In Calibre-web, versions 0.6.0 to 0.6.13 are vulnerable to Cross-Site Request Forgery (CSRF). By luring an authenticated user to click on a link, an attacker can create a new user role with admin privileges and attacker-controlled credentials, allowing them to take over the application.
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.py
cps/__init__.py
cps/about.py
cps/kobo.py
cps/static/js/edit_books.js
cps/static/js/main.js
cps/templates/book_edit.html
cps/templates/book_table.html
cps/templates/config_db.html
cps/templates/config_edit.html
cps/templates/config_view_edit.html
cps/templates/detail.html
cps/templates/email_edit.html
cps/templates/http_error.html
cps/templates/layout.html
cps/templates/list.html
cps/templates/login.html
cps/templates/register.html
cps/templates/search_form.html
cps/templates/shelf_edit.html
cps/templates/user_edit.html
cps/templates/user_table.html
cps/web.py
requirements.txt
setup.cfg
Commit message: Added handling for missing flask-wtf dependency
Added CSRF protection (via flask-wtf)
Moved upload function to js file
Fixed error page in case of csrf failure


Vulnerable code:
except ImportError:
    Unidecode = unidecode_version,
    Scholarly = scholarly_version,
    Flask_SimpleLDAP =  u'installed' if bool(services.ldap) else None,
    python_LDAP = services.ldapVersion if bool(services.ldapVersion) else None,
    Goodreads = u'installed' if bool(services.goodreads_support) else None,
    jsonschema = services.SyncToken.__version__  if bool(services.SyncToken) else None,
    flask_dance = flask_danceVersion,
    greenlet = greenlet_Version
from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub

    language: language
            data: {config_calibre_dir: $("#config_calibre_dir").val()},
            url: window.location.pathname + "/../ajax/view",
  <form role="form" method="POST" autocomplete="off" >
<div class="panel-group class="col-md-10 col-lg-6">
    <div class="form-group required">
      <label for="domainname_deny">{{_('Add Domain')}}</label>
      <input type="text" class="form-control" name="domainname" id="domainname_deny" >
    </div>
    <button id="domain_deny_submit" class="btn btn-default">{{_('Add')}}</button>
<html class="http-error" lang="{{ g.user.locale }}">
                    <form id="form-upload" class="navbar-form" action="{{ url_for('editbook.upload') }}" method="post" enctype="multipart/form-data">
    <script type="text/javascript">
        $(function() {
            $("#form-upload").uploadprogress({
              redirect_url: "{{ url_for('web.index')}}",
              uploadedMsg: "{{_('Upload done, processing, please wait...')}}",
              modalTitle: "{{_('Uploading...')}}",
              modalFooter: "{{_('Close')}}",
              modalTitleFailed: "{{_('Error')}}"
            });
        });
    </script>
    resp.headers['Content-Security-Policy'] = "default-src 'self' 'unsafe-inline' 'unsafe-eval';"
        resp.headers['Content-Security-Policy'] += "img-src * data:"
    # log.debug(request.full_path)
    Programming Language :: Python :: 3.5

Fixed code:
except (ImportError, AttributeError):   # Catch also error for not installed flask-wtf (missing csrf decorator)
try:
    from flask_wtf.csrf import CSRFProtect
    wtf_present = True
except ImportError:
    wtf_present = False

if wtf_present:
    csrf = CSRFProtect()
    csrf.init_app(app)
else:
    csrf = None

    if not wtf_present:
        log.info('*** "flask-wtf" is needed for calibre-web to run. Please install it using pip: "pip install flask-wtf" ***')
        print('*** "flask-wtf" is needed for calibre-web to run. Please install it using pip: "pip install flask-wtf" ***')
        sys.exit(7)

try:
    from flask_wtf import __version__ as flaskwtf_version
except ImportError:
    flaskwtf_version = _(u'not installed')
    Flask_WTF=flaskwtf_version,
    Unidecode=unidecode_version,
    Scholarly=scholarly_version,
    Flask_SimpleLDAP=u'installed' if bool(services.ldap) else None,
    python_LDAP=services.ldapVersion if bool(services.ldapVersion) else None,
    Goodreads=u'installed' if bool(services.goodreads_support) else None,
    jsonschema=services.SyncToken.__version__ if bool(services.SyncToken) else None,
    flask_dance=flask_danceVersion,
    greenlet=greenlet_Version

from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub, csrf
@csrf.exempt
@csrf.exempt
@csrf.exempt
@csrf.exempt
@csrf.exempt
@csrf.exempt
$("#form-upload").uploadprogress({
    redirect_url: getPath() + "/", //"{{ url_for('web.index')}}",
    uploadedMsg: $("#form-upload").data("message"), //"{{_('Upload done, processing, please wait...')}}",
    modalTitle: $("#form-upload").data("title"), //"{{_('Uploading...')}}",
    modalFooter: $("#form-upload").data("footer"), //"{{_('Close')}}",
    modalTitleFailed: $("#form-upload").data("failed") //"{{_('Error')}}"
});

    // equip all post requests with csrf_token
    var csrftoken = $("input[name='csrf_token']").val();
    $.ajaxSetup({
        beforeSend: function(xhr, settings) {
            if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
                xhr.setRequestHeader("X-CSRFToken", csrftoken)
            }
        }
    });

            data: {config_calibre_dir: $("#config_calibre_dir").val(), csrf_token: $("input[name='csrf_token']").val()},
            url: getPath() + "/ajax/view",
          <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
      <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
       <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<form role="form" method="POST" autocomplete="off" >
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="panel-group" class="col-md-10 col-lg-6">
            <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
              <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
      <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
      <div class="form-group required">
        <label for="domainname_deny">{{_('Add Domain')}}</label>
        <input type="text" class="form-control" name="domainname" id="domainname_deny" >
      </div>
      <button id="domain_deny_submit" class="btn btn-default">{{_('Add')}}</button>
<html class="http-error">
                    <form id="form-upload" class="navbar-form" action="{{ url_for('editbook.upload') }}" data-title="{{_('Uploading...')}}" data-footer="{{_('Close')}}" data-failed="{{_('Error')}}" data-message="{{_('Upload done, processing, please wait...')}}" method="post" enctype="multipart/form-data">
      <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
    resp.headers['Content-Security-Policy'] = "default-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data:"
        resp.headers['Content-Security-Policy'] += " *"
flask-wtf>=0.15.0,<0.16.0
    Programming Language :: Python :: 3.8
    Programming Language :: Python :: 3.9
    Programming Language :: Python :: 3.10
    flask-wtf>=0.15.0,<0.16.0