CVE-2022-0339: Server-Side Request Forgery (SSRF) (CWE-918)
value: Server-Side Request Forgery (SSRF) in Pypi calibreweb prior to 0.6.16.
CWE description: The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
CVSS v3 base score: 9.8
Published: 2022-01-30T14:15Z
Repository: https://github.com/janeczku/calibre-web
Language: Python
Files changed: cps/admin.py, cps/editbooks.py, cps/kobo_auth.py, cps/static/js/main.js, cps/templates/generate_kobo_auth_url.html
Commit date: 2022-01-24 18:47:34 +0100
Commit message: Kobo sync token is now also created if accessed from localhost(fixes #1990)
Create kobo sync token button is now "unclicked" after closing dialog
Additional localhost route is catched
If book format is deleted this also deletes the book synced to kobo status

Diff: diff --git a/cps/admin.py b/cps/admin.py
index 706dce394..c1931ea47 100644
--- a/cps/admin.py
+++ b/cps/admin.py
@@ -1424,18 +1424,18 @@ def _delete_user(content):
             # delete KoboReadingState and all it's children
             kobo_entries = ub.session.query(ub.KoboReadingState).filter(ub.KoboReadingState.user_id == content.id).all()
             for kobo_entry in kobo_entries:
                 ub.session.delete(kobo_entry)
             ub.session_commit()
-            log.info(u"User {} deleted".format(content.name))
-            return(_(u"User '%(nick)s' deleted", nick=content.name))
+            log.info("User {} deleted".format(content.name))
+            return(_("User '%(nick)s' deleted", nick=content.name))
         else:
-            log.warning(_(u"Can't delete Guest User"))
-            raise Exception(_(u"Can't delete Guest User"))
+            log.warning(_("Can't delete Guest User"))
+            raise Exception(_("Can't delete Guest User"))
     else:
-        log.warning(u"No admin user remaining, can't delete user")
-        raise Exception(_(u"No admin user remaining, can't delete user"))
+        log.warning("No admin user remaining, can't delete user")
+        raise Exception(_("No admin user remaining, can't delete user"))
 
 
 def _handle_edit_user(to_save, content, languages, translations, kobo_support):
     if to_save.get("delete"):
         try:
diff --git a/cps/editbooks.py b/cps/editbooks.py
index 182484166..8ea004694 100755
--- a/cps/editbooks.py
+++ b/cps/editbooks.py
@@ -339,10 +339,11 @@ def delete_book_from_table(book_id, book_format, jsonResponse):
                 if not book_format:
                     delete_whole_book(book_id, book)
                 else:
                     calibre_db.session.query(db.Data).filter(db.Data.book == book.id).\
                         filter(db.Data.format == book_format).delete()
+                    kobo_sync_status.remove_synced_book(book.id, True)
                 calibre_db.session.commit()
             except Exception as ex:
                 log.debug_or_exception(ex)
                 calibre_db.session.rollback()
                 if jsonResponse:
diff --git a/cps/kobo_auth.py b/cps/kobo_auth.py
index 8d46b26ac..5668e6dad 100644
--- a/cps/kobo_auth.py
+++ b/cps/kobo_auth.py
@@ -116,58 +116,52 @@ kobo_auth = Blueprint("kobo_auth", __name__, url_prefix="/kobo_auth")
 
 
 @kobo_auth.route("/generate_auth_token/<int:user_id>")
 @login_required
 def generate_auth_token(user_id):
+    warning = False
     host_list = request.host.rsplit(':')
     if len(host_list) == 1:
         host = ':'.join(host_list)
     else:
         host = ':'.join(host_list[0:-1])
-    if host.startswith('127.') or host.lower() == 'localhost' or host.startswith('[::ffff:7f'):
-        warning = _('PLease access calibre-web from non localhost to get valid api_endpoint for kobo device')
-        return render_title_template(
-            "generate_kobo_auth_url.html",
-            title=_(u"Kobo Setup"),
-            warning = warning
-        )
-    else:
-        # Invalidate any prevously generated Kobo Auth token for this user.
-        auth_token = ub.session.query(ub.RemoteAuthToken).filter(
-            ub.RemoteAuthToken.user_id == user_id
-        ).filter(ub.RemoteAuthToken.token_type==1).first()
-
-        if not auth_token:
-            auth_token = ub.RemoteAuthToken()
-            auth_token.user_id = user_id
-            auth_token.expiration = datetime.max
-            auth_token.auth_token = (hexlify(urandom(16))).decode("utf-8")
-            auth_token.token_type = 1
-
-            ub.session.add(auth_token)
-            ub.session_commit()
-
-        books = calibre_db.session.query(db.Books).join(db.Data).all()
-
-        for book in books:
-            formats = [data.format for data in book.data]
-            if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats:
-                helper.convert_book_format(book.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name)
-
-        return render_title_template(
-            "generate_kobo_auth_url.html",
-            title=_(u"Kobo Setup"),
-            kobo_auth_url=url_for(
-                "kobo.TopLevelEndpoint", auth_token=auth_token.auth_token, _external=True
-            ),
-            warning = False
-        )
+    if host.startswith('127.') or host.lower() == 'localhost' or host.startswith('[::ffff:7f') or host == "[::1]":
+        warning = _('Please access Calibre-Web from non localhost to get valid api_endpoint for kobo device')
+
+    # Generate auth token if none is existing for this user
+    auth_token = ub.session.query(ub.RemoteAuthToken).filter(
+        ub.RemoteAuthToken.user_id == user_id
+    ).filter(ub.RemoteAuthToken.token_type==1).first()
+
+    if not auth_token:
+        auth_token = ub.RemoteAuthToken()
+        auth_token.user_id = user_id
+        auth_token.expiration = datetime.max
+        auth_token.auth_token = (hexlify(urandom(16))).decode("utf-8")
+        auth_token.token_type = 1
+
+        ub.session.add(auth_token)
+        ub.session_commit()
+
+    books = calibre_db.session.query(db.Books).join(db.Data).all()
+
+    for book in books:
+        formats = [data.format for data in book.data]
+        if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats:
+            helper.convert_book_format(book.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name)
+
+    return render_title_template(
+        "generate_kobo_auth_url.html",
+        title=_(u"Kobo Setup"),
+        auth_token=auth_token.auth_token,
+        warning = warning
+    )
 
 
 @kobo_auth.route("/deleteauthtoken/<int:user_id>", methods=["POST"])
 @login_required
 def delete_auth_token(user_id):
-    # Invalidate any prevously generated Kobo Auth token for this user.
+    # Invalidate any previously generated Kobo Auth token for this user
     ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == user_id)\
         .filter(ub.RemoteAuthToken.token_type==1).delete()
 
     return ub.session_commit()
diff --git a/cps/static/js/main.js b/cps/static/js/main.js
index b10f0f980..75599d9ba 100755
--- a/cps/static/js/main.js
+++ b/cps/static/js/main.js
@@ -533,10 +533,11 @@ $(function() {
             $(this).find(".modal-body").html("...");
         });
 
     $("#modal_kobo_token")
         .on("show.bs.modal", function(e) {
+            $(e.relatedTarget).one('focus', function(e){$(this).blur();});
             var $modalBody = $(this).find(".modal-body");
 
             // Prevent static assets from loading multiple times
             var useCache = function(options) {
                 options.async = true;
diff --git a/cps/templates/generate_kobo_auth_url.html b/cps/templates/generate_kobo_auth_url.html
index fb62424c4..b8b74bda4 100644
--- a/cps/templates/generate_kobo_auth_url.html
+++ b/cps/templates/generate_kobo_auth_url.html
@@ -1,12 +1,15 @@
 {% extends "fragment.html" %}
 {% block body %}
 <div class="well">
-  <p>
-    {{_('Open the .kobo/Kobo eReader.conf file in a text editor and add (or edit):')}}</a>
+<p>
+  {% if not warning %}
+      {{_('Open the .kobo/Kobo eReader.conf file in a text editor and add (or edit):')}}
+    </p><p>
+      api_endpoint={{url_for("kobo.TopLevelEndpoint", auth_token=auth_token, _external=True)}}
+  {% else %}
+      {{warning}}
+    </p><p>{{_('Kobo Token:')}} {{ auth_token }}
+  {% endif %}
   </p>
-  <p>
-    {% if not warning %}api_endpoint={{kobo_auth_url}}{% else %}{{warning}}{% endif %}</a>
-  </p>
-  <p>
 </div>
 {% endblock %}

Vulnerable code:             log.info(u"User {} deleted".format(content.name))
            return(_(u"User '%(nick)s' deleted", nick=content.name))
            log.warning(_(u"Can't delete Guest User"))
            raise Exception(_(u"Can't delete Guest User"))
        log.warning(u"No admin user remaining, can't delete user")
        raise Exception(_(u"No admin user remaining, can't delete user"))
    if host.startswith('127.') or host.lower() == 'localhost' or host.startswith('[::ffff:7f'):
        warning = _('PLease access calibre-web from non localhost to get valid api_endpoint for kobo device')
        return render_title_template(
            "generate_kobo_auth_url.html",
            title=_(u"Kobo Setup"),
            warning = warning
        )
    else:
        # Invalidate any prevously generated Kobo Auth token for this user.
        auth_token = ub.session.query(ub.RemoteAuthToken).filter(
            ub.RemoteAuthToken.user_id == user_id
        ).filter(ub.RemoteAuthToken.token_type==1).first()

        if not auth_token:
            auth_token = ub.RemoteAuthToken()
            auth_token.user_id = user_id
            auth_token.expiration = datetime.max
            auth_token.auth_token = (hexlify(urandom(16))).decode("utf-8")
            auth_token.token_type = 1

            ub.session.add(auth_token)
            ub.session_commit()

        books = calibre_db.session.query(db.Books).join(db.Data).all()

        for book in books:
            formats = [data.format for data in book.data]
            if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats:
                helper.convert_book_format(book.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name)

        return render_title_template(
            "generate_kobo_auth_url.html",
            title=_(u"Kobo Setup"),
            kobo_auth_url=url_for(
                "kobo.TopLevelEndpoint", auth_token=auth_token.auth_token, _external=True
            ),
            warning = False
        )
    # Invalidate any prevously generated Kobo Auth token for this user.
  <p>
    {{_('Open the .kobo/Kobo eReader.conf file in a text editor and add (or edit):')}}</a>
  <p>
    {% if not warning %}api_endpoint={{kobo_auth_url}}{% else %}{{warning}}{% endif %}</a>
  </p>
  <p>
Fixed code:             log.info("User {} deleted".format(content.name))
            return(_("User '%(nick)s' deleted", nick=content.name))
            log.warning(_("Can't delete Guest User"))
            raise Exception(_("Can't delete Guest User"))
        log.warning("No admin user remaining, can't delete user")
        raise Exception(_("No admin user remaining, can't delete user"))
                    kobo_sync_status.remove_synced_book(book.id, True)
    warning = False
    if host.startswith('127.') or host.lower() == 'localhost' or host.startswith('[::ffff:7f') or host == "[::1]":
        warning = _('Please access Calibre-Web from non localhost to get valid api_endpoint for kobo device')

    # Generate auth token if none is existing for this user
    auth_token = ub.session.query(ub.RemoteAuthToken).filter(
        ub.RemoteAuthToken.user_id == user_id
    ).filter(ub.RemoteAuthToken.token_type==1).first()

    if not auth_token:
        auth_token = ub.RemoteAuthToken()
        auth_token.user_id = user_id
        auth_token.expiration = datetime.max
        auth_token.auth_token = (hexlify(urandom(16))).decode("utf-8")
        auth_token.token_type = 1

        ub.session.add(auth_token)
        ub.session_commit()

    books = calibre_db.session.query(db.Books).join(db.Data).all()

    for book in books:
        formats = [data.format for data in book.data]
        if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats:
            helper.convert_book_format(book.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name)

    return render_title_template(
        "generate_kobo_auth_url.html",
        title=_(u"Kobo Setup"),
        auth_token=auth_token.auth_token,
        warning = warning
    )
    # Invalidate any previously generated Kobo Auth token for this user
            $(e.relatedTarget).one('focus', function(e){$(this).blur();});
<p>
  {% if not warning %}
      {{_('Open the .kobo/Kobo eReader.conf file in a text editor and add (or edit):')}}
    </p><p>
      api_endpoint={{url_for("kobo.TopLevelEndpoint", auth_token=auth_token, _external=True)}}
  {% else %}
      {{warning}}
    </p><p>{{_('Kobo Token:')}} {{ auth_token }}
  {% endif %}