Skip to content

Commit e0dcac4

Browse files
committed
validate kernelspec names
allow only: ascii letters, numbers, period, hyphen, underscore.
1 parent f257211 commit e0dcac4

File tree

3 files changed

+57
-4
lines changed

3 files changed

+57
-4
lines changed

docs/kernels.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ locations:
108108
The user location takes priority over the system locations, and the case of the
109109
names is ignored, so selecting kernels works the same way whether or not the
110110
filesystem is case sensitive.
111+
Since kernelspecs show up in URLs and other places,
112+
they are required to be simple names, only containing ASCII letters and numbers,
113+
and the simple separators: ``-``, ``.``, ``_``.
111114

112115
Other locations may also be searched if the :envvar:`JUPYTER_PATH` environment
113116
variable is set.

jupyter_client/kernelspec.py

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import io
77
import json
88
import os
9+
import re
910
import shutil
1011
import warnings
1112

@@ -20,6 +21,7 @@
2021

2122
NATIVE_KERNEL_NAME = 'python3' if PY3 else 'python2'
2223

24+
2325
class KernelSpec(HasTraits):
2426
argv = List()
2527
display_name = Unicode()
@@ -54,19 +56,43 @@ def to_json(self):
5456
"""
5557
return json.dumps(self.to_dict())
5658

59+
60+
_kernel_name_pat = re.compile(r'^[a-z0-9._\-]+$', re.IGNORECASE)
61+
62+
def _is_valid_kernel_name(name):
63+
"""Check that a kernel name is valid."""
64+
# quote is not unicode-safe on Python 2
65+
return _kernel_name_pat.match(name)
66+
67+
68+
_kernel_name_description = "Kernel names can only contain ASCII letters and numbers and these separators: -._"
69+
70+
5771
def _is_kernel_dir(path):
5872
"""Is ``path`` a kernel directory?"""
5973
return os.path.isdir(path) and os.path.isfile(pjoin(path, 'kernel.json'))
6074

75+
6176
def _list_kernels_in(dir):
6277
"""Return a mapping of kernel names to resource directories from dir.
6378
6479
If dir is None or does not exist, returns an empty dict.
6580
"""
6681
if dir is None or not os.path.isdir(dir):
6782
return {}
68-
return {f.lower(): pjoin(dir, f) for f in os.listdir(dir)
69-
if _is_kernel_dir(pjoin(dir, f))}
83+
kernels = {}
84+
for f in os.listdir(dir):
85+
path = pjoin(dir, f)
86+
if not _is_kernel_dir(path):
87+
continue
88+
key = f.lower()
89+
if not _is_valid_kernel_name(key):
90+
warnings.warn("Invalid kernelspec directory name (%s): %s",
91+
_kernel_name_description, path, stacklevel=3,
92+
)
93+
kernels[key] = path
94+
return kernels
95+
7096

7197
class NoSuchKernel(KeyError):
7298
def __init__(self, name):
@@ -75,6 +101,7 @@ def __init__(self, name):
75101
def __str__(self):
76102
return "No such kernel named {}".format(self.name)
77103

104+
78105
class KernelSpecManager(LoggingConfigurable):
79106

80107
kernel_spec_class = Type(KernelSpec, config=True,
@@ -242,10 +269,12 @@ def install_kernel_spec(self, source_dir, kernel_name=None, user=False,
242269
if not kernel_name:
243270
kernel_name = os.path.basename(source_dir)
244271
kernel_name = kernel_name.lower()
245-
272+
if not _is_valid_kernel_name(kernel_name):
273+
raise ValueError("Invalid kernel name %r. %s" % (kernel_name, _kernel_name_description))
274+
246275
if user and prefix:
247276
raise ValueError("Can't specify both user and prefix. Please choose one or the other.")
248-
277+
249278
if replace is not None:
250279
warnings.warn(
251280
"replace is ignored. Installing a kernelspec always replaces an existing installation",

jupyter_client/tests/test_kernelspec.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# coding: utf-8
12
"""Tests for the KernelSpecManager"""
23

34
# Copyright (c) Jupyter Development Team.
@@ -141,3 +142,23 @@ def test_remove_kernel_spec_app(self):
141142
)
142143
out, _ = p.communicate()
143144
self.assertEqual(p.returncode, 0, out.decode('utf8', 'replace'))
145+
146+
def test_validate_kernel_name(self):
147+
for good in [
148+
'julia-0.4',
149+
'ipython',
150+
'R',
151+
'python_3',
152+
'Haskell-1-2-3',
153+
]:
154+
assert kernelspec._is_valid_kernel_name(good)
155+
156+
for bad in [
157+
'has space',
158+
u'ünicode',
159+
'%percent',
160+
'question?',
161+
]:
162+
assert not kernelspec._is_valid_kernel_name(bad)
163+
164+

0 commit comments

Comments
 (0)