1919import tempfile
2020import warnings
2121from getpass import getpass
22+ from contextlib import contextmanager
2223
2324import zmq
2425
3435from jupyter_core .paths import jupyter_data_dir , jupyter_runtime_dir
3536
3637
38+ # TODO: Move to jupyter_core
39+ def win32_restrict_file_to_user (fname ):
40+ """Secure a windows file to read-only access for the user.
41+ Follows guidance from win32 library creator:
42+ http://timgolden.me.uk/python/win32_how_do_i/add-security-to-a-file.html
43+
44+ This method should be executed against an already generated file which
45+ has no secrets written to it yet.
46+
47+ Parameters
48+ ----------
49+
50+ fname : unicode
51+ The path to the file to secure
52+ """
53+ import win32api
54+ import win32security
55+ import ntsecuritycon as con
56+
57+ # everyone, _domain, _type = win32security.LookupAccountName("", "Everyone")
58+ admins , _domain , _type = win32security .LookupAccountName ("" , "Administrators" )
59+ user , _domain , _type = win32security .LookupAccountName ("" , win32api .GetUserName ())
60+
61+ sd = win32security .GetFileSecurity (fname , win32security .DACL_SECURITY_INFORMATION )
62+
63+ dacl = win32security .ACL ()
64+ # dacl.AddAccessAllowedAce(win32security.ACL_REVISION, con.FILE_ALL_ACCESS, everyone)
65+ dacl .AddAccessAllowedAce (win32security .ACL_REVISION , con .FILE_GENERIC_READ | con .FILE_GENERIC_WRITE , user )
66+ dacl .AddAccessAllowedAce (win32security .ACL_REVISION , con .FILE_ALL_ACCESS , admins )
67+
68+ sd .SetSecurityDescriptorDacl (1 , dacl , 0 )
69+ win32security .SetFileSecurity (fname , win32security .DACL_SECURITY_INFORMATION , sd )
70+
71+
72+ # TODO: Move to jupyter_core
73+ @contextmanager
74+ def secure_write (fname , binary = False ):
75+ """Opens a file in the most restricted pattern available for
76+ writing content. This limits the file mode to `600` and yields
77+ the resulting opened filed handle.
78+
79+ Parameters
80+ ----------
81+
82+ fname : unicode
83+ The path to the file to write
84+ """
85+ mode = 'wb' if binary else 'w'
86+ open_flag = os .O_CREAT | os .O_WRONLY | os .O_TRUNC
87+ try :
88+ os .remove (fname )
89+ except (IOError , OSError ):
90+ # Skip any issues with the file not existing
91+ pass
92+
93+ if os .name == 'nt' :
94+ # Python on windows does not respect the group and public bits for chmod, so we need
95+ # to take additional steps to secure the contents.
96+ # Touch file pre-emptively to avoid editing permissions in open files in Windows
97+ fd = os .open (fname , os .O_CREAT | os .O_WRONLY | os .O_TRUNC , 0o600 )
98+ os .close (fd )
99+ open_flag = os .O_WRONLY | os .O_TRUNC
100+ win32_restrict_file_to_user (fname )
101+
102+ with os .fdopen (os .open (fname , open_flag , 0o600 ), mode ) as f :
103+ if os .name != 'nt' :
104+ # Enforce that the file got the requested permissions before writing
105+ assert '0600' == oct (stat .S_IMODE (os .stat (fname ).st_mode )).replace ('0o' , '0' )
106+ yield f
107+
108+
37109def write_connection_file (fname = None , shell_port = 0 , iopub_port = 0 , stdin_port = 0 , hb_port = 0 ,
38110 control_port = 0 , ip = '' , key = b'' , transport = 'tcp' ,
39111 signature_scheme = 'hmac-sha256' , kernel_name = ''
@@ -134,7 +206,10 @@ def write_connection_file(fname=None, shell_port=0, iopub_port=0, stdin_port=0,
134206 cfg ['signature_scheme' ] = signature_scheme
135207 cfg ['kernel_name' ] = kernel_name
136208
137- with open (fname , 'w' ) as f :
209+ # Only ever write this file as user read/writeable
210+ # This would otherwise introduce a vulnerability as a file has secrets
211+ # which would let others execute arbitrarily code as you
212+ with secure_write (fname ) as f :
138213 f .write (json .dumps (cfg , indent = 2 ))
139214
140215 if hasattr (stat , 'S_ISVTX' ):
@@ -193,7 +268,7 @@ def find_connection_file(filename='kernel-*.json', path=None, profile=None):
193268 path = ['.' , jupyter_runtime_dir ()]
194269 if isinstance (path , string_types ):
195270 path = [path ]
196-
271+
197272 try :
198273 # first, try explicit name
199274 return filefind (filename , path )
@@ -208,11 +283,11 @@ def find_connection_file(filename='kernel-*.json', path=None, profile=None):
208283 else :
209284 # accept any substring match
210285 pat = '*%s*' % filename
211-
286+
212287 matches = []
213288 for p in path :
214289 matches .extend (glob .glob (os .path .join (p , pat )))
215-
290+
216291 matches = [ os .path .abspath (m ) for m in matches ]
217292 if not matches :
218293 raise IOError ("Could not find %r in %r" % (filename , path ))
@@ -289,11 +364,11 @@ def tunnel_to_kernel(connection_info, sshserver, sshkey=None):
289364
290365class ConnectionFileMixin (LoggingConfigurable ):
291366 """Mixin for configurable classes that work with connection files"""
292-
367+
293368 data_dir = Unicode ()
294369 def _data_dir_default (self ):
295370 return jupyter_data_dir ()
296-
371+
297372 # The addresses for the communication channels
298373 connection_file = Unicode ('' , config = True ,
299374 help = """JSON file in which to store connection info [default: kernel-<pid>.json]
@@ -480,7 +555,7 @@ def write_connection_file(self):
480555
481556 def load_connection_file (self , connection_file = None ):
482557 """Load connection info from JSON dict in self.connection_file.
483-
558+
484559 Parameters
485560 ----------
486561 connection_file: unicode, optional
@@ -496,10 +571,10 @@ def load_connection_file(self, connection_file=None):
496571
497572 def load_connection_info (self , info ):
498573 """Load connection info from a dict containing connection info.
499-
574+
500575 Typically this data comes from a connection file
501576 and is called by load_connection_file.
502-
577+
503578 Parameters
504579 ----------
505580 info: dict
0 commit comments