Skip to content

Commit a161ffa

Browse files
authored
Merge pull request from GHSA-v7vq-3x77-87vg
add checks for hidden file or path on file get
2 parents 920c5cc + b79702c commit a161ffa

File tree

4 files changed

+345
-6
lines changed

4 files changed

+345
-6
lines changed

notebook/services/contents/filemanager.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,12 @@ def _base_model(self, path):
242242
os_path = self._get_os_path(path)
243243
info = os.lstat(os_path)
244244

245+
four_o_four = "file or directory does not exist: %r" % path
246+
247+
if is_hidden(os_path, self.root_dir) and not self.allow_hidden:
248+
self.log.info("Refusing to serve hidden file or directory %r, via 404 Error", os_path)
249+
raise web.HTTPError(404, four_o_four)
250+
245251
try:
246252
# size of file
247253
size = info.st_size
@@ -363,6 +369,7 @@ def _file_model(self, path, content=True, format=None):
363369
model['type'] = 'file'
364370

365371
os_path = self._get_os_path(path)
372+
366373
model['mimetype'] = mimetypes.guess_type(os_path)[0]
367374

368375
if content:
@@ -423,11 +430,17 @@ def get(self, path, content=True, type=None, format=None):
423430
of the file or directory as well.
424431
"""
425432
path = path.strip('/')
433+
os_path = self._get_os_path(path)
434+
four_o_four = "file or directory does not exist: %r" % path
426435

427436
if not self.exists(path):
428-
raise web.HTTPError(404, f'No such file or directory: {path}')
437+
raise web.HTTPError(404, four_o_four)
429438

430-
os_path = self._get_os_path(path)
439+
if is_hidden(os_path, self.root_dir) and not self.allow_hidden:
440+
self.log.info("Refusing to serve hidden file or directory %r, via 404 Error", os_path)
441+
raise web.HTTPError(404, four_o_four)
442+
443+
431444
if os.path.isdir(os_path):
432445
if type not in (None, 'directory'):
433446
raise web.HTTPError(400,
@@ -446,7 +459,7 @@ def get(self, path, content=True, type=None, format=None):
446459
def _save_directory(self, os_path, model, path=''):
447460
"""create a directory"""
448461
if is_hidden(os_path, self.root_dir) and not self.allow_hidden:
449-
raise web.HTTPError(400, f'Cannot create hidden directory {os_path!r}')
462+
raise web.HTTPError(400, f'Cannot create directory {os_path!r}')
450463
if not os.path.exists(os_path):
451464
with self.perm_to_403():
452465
os.mkdir(os_path)
@@ -465,6 +478,10 @@ def save(self, model, path=''):
465478
raise web.HTTPError(400, 'No file content provided')
466479

467480
os_path = self._get_os_path(path)
481+
482+
if is_hidden(os_path, self.root_dir) and not self.allow_hidden:
483+
raise web.HTTPError(400, f'Cannot create file or directory {os_path!r}')
484+
468485
self.log.debug("Saving %s", os_path)
469486

470487
self.run_pre_save_hook(model=model, path=path)
@@ -508,8 +525,14 @@ def delete_file(self, path):
508525
path = path.strip('/')
509526
os_path = self._get_os_path(path)
510527
rm = os.unlink
511-
if not os.path.exists(os_path):
512-
raise web.HTTPError(404, f'File or directory does not exist: {os_path}')
528+
529+
four_o_four = "file or directory does not exist: %r" % path
530+
531+
if not self.exists(path):
532+
raise web.HTTPError(404, four_o_four)
533+
534+
if is_hidden(os_path, self.root_dir) and not self.allow_hidden:
535+
raise web.HTTPError(400, f'Cannot delete file or directory {os_path!r}')
513536

514537
def is_non_empty_dir(os_path):
515538
if os.path.isdir(os_path):
@@ -552,6 +575,9 @@ def rename_file(self, old_path, new_path):
552575
if new_path == old_path:
553576
return
554577

578+
if (is_hidden(old_path, self.root_dir) or is_hidden(new_path, self.root_dir)) and not self.allow_hidden:
579+
raise web.HTTPError(400, f'Cannot rename file or directory {os_path!r}')
580+
555581
# Perform path validation prior to converting to os-specific value since this
556582
# is still relative to root_dir.
557583
self._validate_path(new_path)

notebook/services/contents/handlers.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ def get(self, path=''):
101101
of the files and directories it contains.
102102
"""
103103
path = path or ''
104+
cm = self.contents_manager
104105
type = self.get_query_argument('type', default=None)
105106
if type not in {None, 'directory', 'file', 'notebook'}:
106107
raise web.HTTPError(400, f'Type {type!r} is invalid')
@@ -112,7 +113,8 @@ def get(self, path=''):
112113
if content not in {'0', '1'}:
113114
raise web.HTTPError(400, f'Content {content!r} is invalid')
114115
content = int(content)
115-
116+
if cm.is_hidden(path) and not cm.allow_hidden:
117+
raise web.HTTPError(404, f'file or directory {path!r} does not exist')
116118
model = yield maybe_future(self.contents_manager.get(
117119
path=path, type=type, format=format, content=content,
118120
))
@@ -125,6 +127,9 @@ def patch(self, path=''):
125127
"""PATCH renames a file or directory without re-uploading content."""
126128
cm = self.contents_manager
127129
model = self.get_json_body()
130+
old_path = model.get('path')
131+
if old_path and (cm.is_hidden(path) or cm.is_hidden(old_path)) and not cm.allow_hidden:
132+
raise web.HTTPError(400, f'Cannot rename file or directory {path!r}')
128133
if model is None:
129134
raise web.HTTPError(400, 'JSON body missing')
130135
model = yield maybe_future(cm.update(model, path))
@@ -193,6 +198,9 @@ def post(self, path=''):
193198
raise web.HTTPError(404, f"No such directory: {path}")
194199

195200
model = self.get_json_body()
201+
copy_from = model.get('copy_from')
202+
if copy_from and (cm.is_hidden(path) or cm.is_hidden(copy_from)) and not cm.allow_hidden:
203+
raise web.HTTPError(400, f'Cannot copy file or directory {path!r}')
196204

197205
if model is not None:
198206
copy_from = model.get('copy_from')
@@ -219,9 +227,12 @@ def put(self, path=''):
219227
create a new empty notebook.
220228
"""
221229
model = self.get_json_body()
230+
cm = self.contents_manager
222231
if model:
223232
if model.get('copy_from'):
224233
raise web.HTTPError(400, "Cannot copy with PUT, only POST")
234+
if model.get('path') and (cm.is_hidden(path) or cm.is_hidden(model.get('path'))) and not cm.allow_hidden:
235+
raise web.HTTPError(400, f'Cannot create file or directory {path!r}')
225236
exists = yield maybe_future(self.contents_manager.file_exists(path))
226237
if exists:
227238
yield maybe_future(self._save(model, path))
@@ -235,6 +246,10 @@ def put(self, path=''):
235246
def delete(self, path=''):
236247
"""delete a file in the given path"""
237248
cm = self.contents_manager
249+
250+
if cm.is_hidden(path) and not cm.allow_hidden:
251+
raise web.HTTPError(400, f'Cannot delete file or directory {path!r}')
252+
238253
self.log.warning('delete %s', path)
239254
yield maybe_future(cm.delete(path))
240255
self.set_status(204)

notebook/services/contents/tests/test_contents_api.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,16 @@ def test_get_contents_no_such_file(self):
321321
with assert_http_error(404):
322322
self.api.read('foo/q.ipynb')
323323

324+
def test_get_404_hidden(self):
325+
if sys.platform == 'win32':
326+
self.skipTest("Disabled copying hidden files on Windows")
327+
self.make_txt('.hidden/visible.txt', 'test string')
328+
self.make_txt('.hidden.txt', 'test string')
329+
with assert_http_error(404):
330+
resp = self.api.read('.hidden/visible.txt')
331+
with assert_http_error(404):
332+
resp = self.api.read('.hidden.txt')
333+
324334
def test_get_text_file_contents(self):
325335
for d, name in self.dirs_nbs:
326336
path = url_path_join(d, name + '.txt')
@@ -443,6 +453,51 @@ def test_upload_txt(self):
443453
self.assertEqual(model['format'], 'text')
444454
self.assertEqual(model['content'], body)
445455

456+
def test_upload_txt_hidden(self):
457+
if sys.platform == 'win32':
458+
self.skipTest("Disabled copying hidden files on Windows")
459+
with assert_http_error(400):
460+
body = 'ünicode téxt'
461+
model = {
462+
'content' : body,
463+
'format' : 'text',
464+
'type' : 'file',
465+
}
466+
path = '.hidden/Upload tést.txt'
467+
resp = self.api.upload(path, body=json.dumps(model))
468+
469+
with assert_http_error(400):
470+
body = 'ünicode téxt'
471+
model = {
472+
'content' : body,
473+
'format' : 'text',
474+
'type' : 'file',
475+
'path': '.hidden/test.txt'
476+
}
477+
path = 'Upload tést.txt'
478+
resp = self.api.upload(path, body=json.dumps(model))
479+
480+
with assert_http_error(400):
481+
body = 'ünicode téxt'
482+
model = {
483+
'content' : body,
484+
'format' : 'text',
485+
'type' : 'file',
486+
}
487+
path = '.hidden.txt'
488+
resp = self.api.upload(path, body=json.dumps(model))
489+
490+
with assert_http_error(400):
491+
body = 'ünicode téxt'
492+
model = {
493+
'content' : body,
494+
'format' : 'text',
495+
'type' : 'file',
496+
'path': '.hidden.txt'
497+
}
498+
path = 'Upload tést.txt'
499+
resp = self.api.upload(path, body=json.dumps(model))
500+
446501
def test_upload_b64(self):
447502
body = b'\xFFblob'
448503
b64body = encodebytes(body).decode('ascii')
@@ -497,10 +552,37 @@ def test_copy_path(self):
497552
resp = self.api.copy('foo/a.ipynb', 'å b')
498553
self._check_created(resp, 'å b/a-Copy1.ipynb')
499554

555+
def test_copy_400_hidden(self):
556+
if sys.platform == 'win32':
557+
self.skipTest("Disabled copying hidden files on Windows")
558+
self.make_txt('new.txt', 'test string')
559+
self.make_txt('.hidden/new.txt', 'test string')
560+
self.make_txt('.hidden.txt', 'test string')
561+
with assert_http_error(400):
562+
resp = self.api.copy('.hidden/old.txt', 'new.txt')
563+
with assert_http_error(400):
564+
resp = self.api.copy('old.txt', '.hidden/new.txt')
565+
with assert_http_error(400):
566+
resp = self.api.copy('.hidden.txt', 'new.txt')
567+
with assert_http_error(400):
568+
resp = self.api.copy('old.txt', '.hidden.txt')
569+
500570
def test_copy_put_400(self):
501571
with assert_http_error(400):
502572
resp = self.api.copy_put('å b/ç d.ipynb', 'å b/cøpy.ipynb')
503573

574+
def test_copy_put_400_hidden(self):
575+
if sys.platform == 'win32':
576+
self.skipTest("Disabled copying hidden files on Windows")
577+
with assert_http_error(400):
578+
resp = self.api.copy_put('.hidden/old.txt', 'new.txt')
579+
with assert_http_error(400):
580+
resp = self.api.copy_put('old.txt', '.hidden/new.txt')
581+
with assert_http_error(400):
582+
resp = self.api.copy_put('.hidden.txt', 'new.txt')
583+
with assert_http_error(400):
584+
resp = self.api.copy_put('old.txt', '.hidden.txt')
585+
504586
def test_copy_dir_400(self):
505587
# can't copy directories
506588
with assert_http_error(400):
@@ -543,6 +625,29 @@ def test_delete_non_empty_dir(self):
543625
with assert_http_error(404):
544626
self.api.list('å b')
545627

628+
def test_delete_hidden_dir(self):
629+
if sys.platform == 'win32':
630+
self.skipTest("Disabled deleting hidden dirs on Windows")
631+
with assert_http_error(400):
632+
# Test that non empty directory can be deleted
633+
try:
634+
self.api.delete('.hidden')
635+
except requests.HTTPError as e:
636+
assert e.response.status_code == 400
637+
raise e
638+
639+
def test_delete_hidden_file(self):
640+
#Test deleting file in a hidden directory
641+
if sys.platform == 'win32':
642+
self.skipTest("Disabled deleting hidden dirs on Windows")
643+
with assert_http_error(400):
644+
# Test that non empty directory can be deleted
645+
self.api.delete('.hidden/test.txt')
646+
647+
#Test deleting a hidden file
648+
with assert_http_error(400):
649+
self.api.delete('.hidden.txt')
650+
546651
def test_rename(self):
547652
resp = self.api.rename('foo/a.ipynb', 'foo/z.ipynb')
548653
self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
@@ -555,6 +660,21 @@ def test_rename(self):
555660
self.assertIn('z.ipynb', nbnames)
556661
self.assertNotIn('a.ipynb', nbnames)
557662

663+
def test_rename_400_hidden(self):
664+
if sys.platform == 'win32':
665+
self.skipTest("Disabled copying hidden files on Windows")
666+
# self.make_txt('new.txt', 'test string')
667+
# self.make_txt('.hidden/new.txt', 'test string')
668+
# self.make_txt('.hidden.txt', 'test string')
669+
with assert_http_error(400):
670+
resp = self.api.rename('.hidden/old.txt', 'new.txt')
671+
with assert_http_error(400):
672+
resp = self.api.rename('old.txt', '.hidden/new.txt')
673+
with assert_http_error(400):
674+
resp = self.api.rename('.hidden.txt', 'new.txt')
675+
with assert_http_error(400):
676+
resp = self.api.rename('old.txt', '.hidden.txt')
677+
558678
def test_checkpoints_follow_file(self):
559679

560680
# Read initial file state

0 commit comments

Comments
 (0)