Security in the Jupyter Server#
Since access to the Jupyter Server means access to running arbitrary code, it is important to restrict access to the server. For this reason, Jupyter Server uses a token-based authentication that is on by default.
Note
If you enable a password for your server, token authentication is not enabled by default.
When token authentication is enabled, the server uses a token to authenticate requests. This token can be provided to login to the server in three ways:
in the
Authorizationheader, e.g.:Authorization: token abcdef...
In a URL parameter, e.g.:
https://my-server/tree/?token=abcdef...
In the password field of the login form that will be shown to you if you are not logged in.
When you start a Jupyter server with token authentication enabled (default), a token is generated to use for authentication. This token is logged to the terminal, so that you can copy/paste the URL into your browser:
[I 11:59:16.597 ServerApp] The Jupyter Server is running at:
[I 11:59:16.597 ServerApp]
http://localhost:8888/?token=c8de56fa4deed24899803e93c227592aef6538f93025fe01
[I 11:59:16.597 ServerApp]
http://127.0.0.1:8888/?token=c8de56fa4deed24899803e93c227592aef6538f93025fe01
[I 11:59:16.597 ServerApp]
To access the server, open this file in a browser:
file:///Users/username/Library/Jupyter/runtime/jpserver-46320-open.html
Or copy and paste one of these URLs:
http://localhost:8888/?token=c8de56fa4deed24899803e93c227592aef6538f93025fe01
http://127.0.0.1:8888/?token=c8de56fa4deed24899803e93c227592aef6538f93025fe01
Copy either of the HTTP URLs and paste it into your browser to see the server running with a message - “A Jupyter Server is running.” If you are using the file link, opening it in your browser should automatically redirect you to the Jupyter server launch page, including the authentication token. In case it doesn’t redirect automatically, you’ll find an HTTP link on the page; clicking this link will take you to the Jupyter server landing page.
At any later time, you can see the tokens and URLs for all of your running servers with jupyter server list:
$ jupyter server list
Currently running servers:
http://localhost:8888/?token=abc... :: /home/you/notebooks
https://0.0.0.0:9999/?token=123... :: /tmp/public
http://localhost:8889/ :: /tmp/has-password
For servers with token-authentication enabled, the URL in the above listing will include the token, so you can copy and paste that URL into your browser to login. If a server has no token (e.g. it has a password or has authentication disabled), the URL will not include the token argument. Once you have visited this URL, a cookie will be set in your browser and you won’t need to use the token again, unless you switch browsers, clear your cookies, or start a Jupyter server on a new port.
Alternatives to token authentication#
If a generated token doesn’t work well for you,
you can set a password for your server.
jupyter server password will prompt you for a password,
and store the hashed password in your jupyter_server_config.json.
It is possible to disable authentication altogether by setting the token and password to empty strings, but this is NOT RECOMMENDED, unless authentication or access restrictions are handled at a different layer in your web application:
c.ServerApp.token = ""
c.ServerApp.password = ""
Kernel transport encryption#
Added in version 2.20.
By default, ZMQ sockets used to communicate with kernels (shell, IOPub, stdin, control, heartbeat) are bound to TCP ports with no transport-level encryption. Any process on the same host that can reach those ports can connect and read messages, including all IOPub output.
CurveZMQ adds elliptic-curve
encryption and authentication directly at the ZMQ transport layer.
When enabled, the KernelManager generates a keypair, writes the keys into
the kernel’s connection file, and configures all sockets as a CurveZMQ server.
Clients must present the correct server public key to connect; unauthenticated
connections are silently dropped before any data is delivered.
Note
CurveZMQ is only available when pyzmq was compiled against a libzmq that includes libsodium. You can verify this with:
python -c "import zmq; print(zmq.has('curve'))"
If this prints False, the transport_encryption setting has no
effect and attempts to set it to 'auto' or 'required' will raise
a configuration error.
The transport_encryption setting#
Transport encryption is controlled by the KernelManager.transport_encryption
traitlet, which accepts three values:
'disabled'(default)No CurveZMQ keys are generated. All kernel sockets are unencrypted.
'auto'Keys are generated only when the kernelspec declares support via
metadata.supported_encryption: ['curve']. Kernelspecs that do not declare this field are started without encryption, so the setting is safe to enable globally without breaking existing kernels.'required'Keys are always generated. Startup fails with a
RuntimeErrorif the kernelspec does not declaremetadata.supported_encryption: ['curve'], so kernels that have not been updated to handle the connection-file keys are never started unencrypted.
Note
transport_encryption applies to both the tcp and ipc
transports. IPC sockets also rely on filesystem permissions for access
control, but CurveZMQ can be layered on top for defense in depth.
To enable encryption globally for all kernels that support it, add the
following to your jupyter_server_config.py:
c.KernelManager.transport_encryption = "auto"
To enforce encryption and prevent unencrypted kernels from starting:
c.KernelManager.transport_encryption = "required"
Kernelspec requirements#
A kernel must declare CurveZMQ support in its kernel.json before the
KernelManager will provision keys for it:
{
"argv": ["python", "-m", "ipykernel_launcher", "-f", "{connection_file}"],
"display_name": "Python 3",
"language": "python",
"metadata": {
"supported_encryption": ["curve"]
}
}
When transport_encryption is 'auto', kernelspecs without this field are
started normally without encryption. When it is 'required', their startup
is refused.
The kernel itself must read curve_publickey and curve_secretkey from
the connection file and apply them to its ZMQ sockets. Kernels based on
ipykernel 7.3 do this automatically when the fields are present.
Note
When updating a previously installed kernel to a version that supports encryption
you may need to re-install the kernelspec or manually add the supported_encryption
metadata field. If you subsequently decide to downgrade, you will need to
remove this field as otherwise the kernel will silently fail to connect.
Security in notebook documents#
As Jupyter Server become more popular for sharing and collaboration, the potential for malicious people to attempt to exploit the notebook for their nefarious purposes increases. IPython 2.0 introduced a security model to prevent execution of untrusted code without explicit user input.
The problem#
The whole point of Jupyter is arbitrary code execution. We have no desire to limit what can be done with a notebook, which would negatively impact its utility.
Unlike other programs, a Jupyter notebook document includes output. Unlike other documents, that output exists in a context that can execute code (via Javascript).
The security problem we need to solve is that no code should execute just because a user has opened a notebook that they did not write. Like any other program, once a user decides to execute code in a notebook, it is considered trusted, and should be allowed to do anything.
Our security model#
Untrusted HTML is always sanitized
Untrusted Javascript is never executed
HTML and Javascript in Markdown cells are never trusted
Outputs generated by the user are trusted
Any other HTML or Javascript (in Markdown cells, output generated by others) is never trusted
The central question of trust is “Did the current user do this?”
The details of trust#
When a notebook is executed and saved, a signature is computed from a digest of the notebook’s contents plus a secret key. This is stored in a database, writable only by the current user. By default, this is located at:
~/.local/share/jupyter/nbsignatures.db # Linux
~/Library/Jupyter/nbsignatures.db # OS X
%APPDATA%/jupyter/nbsignatures.db # Windows
Each signature represents a series of outputs which were produced by code the current user executed, and are therefore trusted.
When you open a notebook, the server computes its signature, and checks if it’s in the database. If a match is found, HTML and Javascript output in the notebook will be trusted at load, otherwise it will be untrusted.
Any output generated during an interactive session is trusted.
Updating trust#
A notebook’s trust is updated when the notebook is saved. If there are
any untrusted outputs still in the notebook, the notebook will not be
trusted, and no signature will be stored. If all untrusted outputs have
been removed (either via Clear Output or re-execution), then the
notebook will become trusted.
While trust is updated per output, this is only for the duration of a single session. A newly loaded notebook file is either trusted or not in its entirety.
Explicit trust#
Sometimes re-executing a notebook to generate trusted output is not an option, either because dependencies are unavailable, or it would take a long time. Users can explicitly trust a notebook in two ways:
At the command-line, with:
jupyter trust /path/to/notebook.ipynb
After loading the untrusted notebook, with
File / Trust Notebook
These two methods simply load the notebook, compute a new signature, and add that signature to the user’s database.
Content-Security-Policy for nbconvert#
When notebooks are rendered via nbconvert (/nbconvert/ endpoints),
the server adds a sandbox allow-scripts directive to the
Content-Security-Policy header by default. This confines any
JavaScript in the rendered output to a unique origin, preventing it
from interacting with the Jupyter server.
This behavior is controlled by nbconvert_csp_sandbox:
# jupyter_server_config.py
c.ServerApp.nbconvert_csp_sandbox = True # default
Reporting security issues#
If you find a security vulnerability in Jupyter, either a failure of the code to properly implement the model described here, or a failure of the model itself, please report it to security@ipython.org.
Affected use cases#
Some use cases that work in Jupyter 1.0 became less convenient in 2.0 as a result of the security changes. We do our best to minimize these annoyances, but security is always at odds with convenience.
Javascript and CSS in Markdown cells#
While never officially supported, it had become common practice to put hidden Javascript or CSS styling in Markdown cells, so that they would not be visible on the page. Since Markdown cells are now sanitized (by Google Caja), all Javascript (including click event handlers, etc.) and CSS will be stripped.
We plan to provide a mechanism for notebook themes, but in the meantime
styling the notebook can only be done via either custom.css or CSS
in HTML output. The latter only have an effect if the notebook is
trusted, because otherwise the output will be sanitized just like
Markdown.
Collaboration#
When collaborating on a notebook, people probably want to see the outputs produced by their colleagues’ most recent executions. Since each collaborator’s key will differ, this will result in each share starting in an untrusted state. There are three basic approaches to this:
re-run notebooks when you get them (not always viable)
explicitly trust notebooks via
jupyter trustor the notebook menu (annoying, but easy)share a notebook signatures database, and use configuration dedicated to the collaboration while working on the project.
To share a signatures database among users, you can configure:
c.NotebookNotary.data_dir = "/path/to/signature_dir"
to specify a non-default path to the SQLite database (of notebook hashes, essentially).