CVE-2020-15110: Incorrect Authorization (CWE-863)
value: In jupyterhub-kubespawner before 0.12, certain usernames will be able to craft particular server names which will grant them access to the default server of other users who have matching usernames. This has been fixed in 0.12.
CWE description: The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. This allows attackers to bypass intended access restrictions.
CVSS v3 base score: 8.1
Published: 2020-07-17T21:15Z
Repository: https://github.com/jupyterhub/kubespawner
Language: Python
Files changed: kubespawner/spawner.py
Commit date: 2020-07-03 09:48:42 +0200
Commit message: move delimiter to pvc/pod name templates

and note version change

Diff: diff --git a/kubespawner/spawner.py b/kubespawner/spawner.py
index f171ec6..2106cd3 100644
--- a/kubespawner/spawner.py
+++ b/kubespawner/spawner.py
@@ -305,20 +305,29 @@ class KubeSpawner(Spawner):
         give arbitrary users root over your entire cluster.
         """
     )
 
     pod_name_template = Unicode(
-        'jupyter-{username}{servername}',
+        'jupyter-{username}--{servername}',
         config=True,
         help="""
         Template to use to form the name of user's pods.
 
-        `{username}` is expanded to the escaped, dns-label safe username.
+        `{username}` is expanded to the escaped, dns-label-safe username.
+        `{servername}` is expanded to the escaped, dns-label-safe server name, if any.
+
+        Trailing `-` characters are stripped for safe handling of empty server names (user default servers).
 
         This must be unique within the namespace the pods are being spawned
         in, so if you are running multiple jupyterhubs spawning in the
         same namespace, consider setting this to be something more unique.
+
+        .. versionchanged:: 0.12
+            `--` delimiter added to the template,
+            where it was implicitly added to the `servername` field before.
+            Additionally, `username--servername` delimiter was `-` instead of `--`,
+            allowing collisions in certain circumstances.
         """
     )
 
     storage_pvc_ensure = Bool(
         False,
@@ -330,20 +339,29 @@ class KubeSpawner(Spawner):
         not exist for the user when their pod is spawning.
         """
     )
 
     pvc_name_template = Unicode(
-        'claim-{username}{servername}',
+        'claim-{username}--{servername}',
         config=True,
         help="""
         Template to use to form the name of user's pvc.
 
         `{username}` is expanded to the escaped, dns-label safe username.
+        `{servername}` is expanded to the escaped, dns-label-safe server name, if any.
+
+        Trailing `-` characters are stripped for safe handling of empty server names (user default servers).
 
         This must be unique within the namespace the pvc are being spawned
         in, so if you are running multiple jupyterhubs spawning in the
         same namespace, consider setting this to be something more unique.
+
+        .. versionchanged:: 0.12
+            `--` delimiter added to the template,
+            where it was implicitly added to the `servername` field before.
+            Additionally, `username--servername` delimiter was `-` instead of `--`,
+            allowing collisions in certain circumstances.
         """
     )
 
     # FIXME: Don't override 'default_value' ("") or 'allow_none' (False) (Breaking change)
     hub_connect_ip = Unicode(
@@ -1311,32 +1329,26 @@ class KubeSpawner(Spawner):
     def _expand_user_properties(self, template):
         # Make sure username and servername match the restrictions for DNS labels
         # Note: '-' is not in safe_chars, as it is being used as escape character
         safe_chars = set(string.ascii_lowercase + string.digits)
 
-        # Set servername based on whether named-server initialised
-        if self.name:
-            # use two -- to ensure no collision possibilities
-            # are created by an ambiguous boundary between username and
-            # servername.
-            # -- cannot occur in a string where - is the escape char.
-            servername = '--{}'.format(self.name)
-            safe_servername = '--{}'.format(escapism.escape(self.name, safe=safe_chars, escape_char='-').lower())
-        else:
-            servername = ''
-            safe_servername = ''
+        raw_servername = self.name or ''
+        safe_servername = escapism.escape(raw_servername, safe=safe_chars, escape_char='-').lower()
 
         legacy_escaped_username = ''.join([s if s in safe_chars else '-' for s in self.user.name.lower()])
         safe_username = escapism.escape(self.user.name, safe=safe_chars, escape_char='-').lower()
-        return template.format(
+        rendered = template.format(
             userid=self.user.id,
             username=safe_username,
             unescaped_username=self.user.name,
             legacy_escape_username=legacy_escaped_username,
             servername=safe_servername,
-            unescaped_servername=servername,
+            unescaped_servername=raw_servername,
         )
+        # strip trailing - delimiter in case of empty servername.
+        # k8s object names cannot have trailing -
+        return rendered.rstrip("-")
 
     def _expand_all(self, src):
         if isinstance(src, list):
             return [self._expand_all(i) for i in src]
         elif isinstance(src, dict):

Vulnerable code:         'jupyter-{username}{servername}',
        `{username}` is expanded to the escaped, dns-label safe username.
        'claim-{username}{servername}',
        # Set servername based on whether named-server initialised
        if self.name:
            # use two -- to ensure no collision possibilities
            # are created by an ambiguous boundary between username and
            # servername.
            # -- cannot occur in a string where - is the escape char.
            servername = '--{}'.format(self.name)
            safe_servername = '--{}'.format(escapism.escape(self.name, safe=safe_chars, escape_char='-').lower())
        else:
            servername = ''
            safe_servername = ''
        return template.format(
            unescaped_servername=servername,
Fixed code:         'jupyter-{username}--{servername}',
        `{username}` is expanded to the escaped, dns-label-safe username.
        `{servername}` is expanded to the escaped, dns-label-safe server name, if any.

        Trailing `-` characters are stripped for safe handling of empty server names (user default servers).

        .. versionchanged:: 0.12
            `--` delimiter added to the template,
            where it was implicitly added to the `servername` field before.
            Additionally, `username--servername` delimiter was `-` instead of `--`,
            allowing collisions in certain circumstances.
        'claim-{username}--{servername}',
        `{servername}` is expanded to the escaped, dns-label-safe server name, if any.

        Trailing `-` characters are stripped for safe handling of empty server names (user default servers).

        .. versionchanged:: 0.12
            `--` delimiter added to the template,
            where it was implicitly added to the `servername` field before.
            Additionally, `username--servername` delimiter was `-` instead of `--`,
            allowing collisions in certain circumstances.
        raw_servername = self.name or ''
        safe_servername = escapism.escape(raw_servername, safe=safe_chars, escape_char='-').lower()
        rendered = template.format(
            unescaped_servername=raw_servername,
        # strip trailing - delimiter in case of empty servername.
        # k8s object names cannot have trailing -
        return rendered.rstrip("-")