Skip to content

Commit

Permalink
Merge pull request from GHSA-w3vc-fx9p-wp4v
Browse files Browse the repository at this point in the history
Backport: Check authentication when websockets open
  • Loading branch information
consideRatio committed Mar 13, 2024
2 parents a82f70a + 836ac5c commit bead903
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 26 deletions.
32 changes: 32 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,37 @@
## 3.2

### 3.2.2 - 2022-09-08

#### Bugs fixed

- add allow-downloads and allow-modals to sandbox [#335](https://github.com/jupyterhub/jupyter-server-proxy/pull/335) ([@djangoliv](https://github.com/djangoliv))
- allow empty PUT body [#331](https://github.com/jupyterhub/jupyter-server-proxy/pull/331) ([@pepijndevos](https://github.com/pepijndevos))
- [bugfix] Hop by hop header handling [#328](https://github.com/jupyterhub/jupyter-server-proxy/pull/328) ([@mahnerak](https://github.com/mahnerak))

#### Documentation improvements

- Yarn link malformed. [#320](https://github.com/jupyterhub/jupyter-server-proxy/pull/320) ([@matthew-brett](https://github.com/matthew-brett))

#### Continuous integration improvements

- Install `notebook<7` for notebook test [#340](https://github.com/jupyterhub/jupyter-server-proxy/pull/340) ([@manics](https://github.com/manics))
- Run publish workflow for tags [#318](https://github.com/jupyterhub/jupyter-server-proxy/pull/318) ([@manics](https://github.com/manics))

#### Dependency updates

- Bump terser from 5.10.0 to 5.14.2 in /jupyterlab-server-proxy [#342](https://github.com/jupyterhub/jupyter-server-proxy/pull/342) ([@dependabot](https://github.com/dependabot))
- Bump moment from 2.29.2 to 2.29.4 in /jupyterlab-server-proxy [#341](https://github.com/jupyterhub/jupyter-server-proxy/pull/341) ([@dependabot](https://github.com/dependabot))
- Bump moment from 2.29.1 to 2.29.2 in /jupyterlab-server-proxy [#336](https://github.com/jupyterhub/jupyter-server-proxy/pull/336) ([@dependabot](https://github.com/dependabot))
- Bump minimist from 1.2.5 to 1.2.6 in /jupyterlab-server-proxy [#334](https://github.com/jupyterhub/jupyter-server-proxy/pull/334) ([@dependabot](https://github.com/dependabot))
- Bump url-parse from 1.5.3 to 1.5.7 in /jupyterlab-server-proxy [#327](https://github.com/jupyterhub/jupyter-server-proxy/pull/327) ([@dependabot](https://github.com/dependabot))

#### Contributors to this release

([GitHub contributors page for this release](https://github.com/jupyterhub/jupyter-server-proxy/graphs/contributors?from=2022-01-24&to=2022-09-08&type=c))

[@austinmw](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-server-proxy+involves%3Aaustinmw+updated%3A2022-01-24..2022-09-08&type=Issues) | [@bollwyvl](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-server-proxy+involves%3Abollwyvl+updated%3A2022-01-24..2022-09-08&type=Issues) | [@consideRatio](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-server-proxy+involves%3AconsideRatio+updated%3A2022-01-24..2022-09-08&type=Issues) | [@dependabot](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-server-proxy+involves%3Adependabot+updated%3A2022-01-24..2022-09-08&type=Issues) | [@djangoliv](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-server-proxy+involves%3Adjangoliv+updated%3A2022-01-24..2022-09-08&type=Issues) | [@jhgoebbert](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-server-proxy+involves%3Ajhgoebbert+updated%3A2022-01-24..2022-09-08&type=Issues) | [@mahnerak](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-server-proxy+involves%3Amahnerak+updated%3A2022-01-24..2022-09-08&type=Issues) | [@manics](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-server-proxy+involves%3Amanics+updated%3A2022-01-24..2022-09-08&type=Issues) | [@matthew-brett](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-server-proxy+involves%3Amatthew-brett+updated%3A2022-01-24..2022-09-08&type=Issues) | [@meeseeksmachine](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-server-proxy+involves%3Ameeseeksmachine+updated%3A2022-01-24..2022-09-08&type=Issues) | [@pepijndevos](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-server-proxy+involves%3Apepijndevos+updated%3A2022-01-24..2022-09-08&type=Issues) | [@ryanlovett](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-server-proxy+involves%3Aryanlovett+updated%3A2022-01-24..2022-09-08&type=Issues) | [@ryshoooo](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-server-proxy+involves%3Aryshoooo+updated%3A2022-01-24..2022-09-08&type=Issues) | [@takluyver](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-server-proxy+involves%3Atakluyver+updated%3A2022-01-24..2022-09-08&type=Issues) | [@yuvipanda](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyter-server-proxy+involves%3Ayuvipanda+updated%3A2022-01-24..2022-09-08&type=Issues)


### 3.2.1 - 2022-01-24

3.2.1 is a security release, fixing a vulnerability [GHSA-gcv9-6737-pjqw](https://github.com/jupyterhub/jupyter-server-proxy/security/advisories/GHSA-gcv9-6737-pjqw) where `allowed_hosts` were not validated correctly.
Expand Down
2 changes: 2 additions & 0 deletions jupyter_server_proxy/__init__.py
Expand Up @@ -3,6 +3,8 @@
from jupyter_server.utils import url_path_join as ujoin
from .api import ServersInfoHandler, IconHandler

__version__ = "3.2.3"

# Jupyter Extension points
def _jupyter_server_extension_points():
return [{
Expand Down
35 changes: 33 additions & 2 deletions jupyter_server_proxy/handlers.py
Expand Up @@ -124,6 +124,39 @@ def check_origin(self, origin=None):
async def open(self, port, proxied_path):
raise NotImplementedError('Subclasses of ProxyHandler should implement open')

async def prepare(self, *args, **kwargs):
"""
Enforce authentication on *all* requests.
This method is called *before* any other method for all requests.
See https://www.tornadoweb.org/en/stable/web.html#tornado.web.RequestHandler.prepare.
"""
# Due to https://github.com/jupyter-server/jupyter_server/issues/1012,
# we can not decorate `prepare` with `@web.authenticated`.
# `super().prepare`, which calls `JupyterHandler.prepare`, *must* be called
# before `@web.authenticated` can work. Since `@web.authenticated` is a decorator
# that relies on the decorated method to get access to request information, we can
# not call it directly. Instead, we create an empty lambda that takes a request_handler,
# decorate that with web.authenticated, and call the decorated function.
# super().prepare became async with jupyter_server v2
_prepared = super().prepare(*args, **kwargs)
if _prepared is not None:
await _prepared

# If this is a GET request that wants to be upgraded to a websocket, users not
# already authenticated gets a straightforward 403. Everything else is dealt
# with by `web.authenticated`, which does a 302 to the appropriate login url.
# Websockets are purely API calls made by JS rather than a direct user facing page,
# so redirects do not make sense for them.
if (
self.request.method == "GET"
and self.request.headers.get("Upgrade", "").lower() == "websocket"
):
if not self.current_user:
raise web.HTTPError(403)
else:
web.authenticated(lambda request_handler: None)(self)

async def http_get(self, host, port, proxy_path=''):
'''Our non-websocket GET.'''
raise NotImplementedError('Subclasses of ProxyHandler should implement http_get')
Expand Down Expand Up @@ -265,7 +298,6 @@ def _check_host_allowlist(self, host):
else:
return host in self.host_allowlist

@web.authenticated
async def proxy(self, host, port, proxied_path):
'''
This serverextension handles:
Expand Down Expand Up @@ -664,7 +696,6 @@ async def ensure_process(self):
raise


@web.authenticated
async def proxy(self, port, path):
if not path.startswith('/'):
path = '/' + path
Expand Down
2 changes: 1 addition & 1 deletion jupyterlab-server-proxy/package.json
@@ -1,6 +1,6 @@
{
"name": "@jupyterlab/server-proxy",
"version": "3.2.2",
"version": "3.2.3",
"description": "Jupyter server extension to supervise and proxy web services",
"keywords": [
"jupyter",
Expand Down
1 change: 1 addition & 0 deletions setup.py
Expand Up @@ -93,6 +93,7 @@
# acceptance tests additionally require firefox and geckodriver
"test": [
"pytest",
"pytest-asyncio",
"pytest-cov",
"pytest-html"
],
Expand Down
41 changes: 18 additions & 23 deletions tests/test_proxies.py
Expand Up @@ -6,6 +6,7 @@
from http.client import HTTPConnection
from urllib.parse import quote
import pytest
from tornado.httpclient import HTTPClientError
from tornado.websocket import websocket_connect

PORT = os.getenv('TEST_PORT', 8888)
Expand Down Expand Up @@ -246,28 +247,19 @@ def test_server_content_encoding_header():
assert f.read() == b'this is a test'


@pytest.fixture(scope="module")
def event_loop():
loop = asyncio.get_event_loop()
yield loop
loop.close()


async def _websocket_echo():
url = "ws://localhost:{}/python-websocket/echosocket".format(PORT)
@pytest.mark.asyncio
async def test_server_proxy_websocket_messages():
url = "ws://localhost:{}/python-websocket/echosocket?token={}".format(PORT, TOKEN)
conn = await websocket_connect(url)
expected_msg = "Hello, world!"
await conn.write_message(expected_msg)
msg = await conn.read_message()
assert msg == expected_msg


def test_server_proxy_websocket(event_loop):
event_loop.run_until_complete(_websocket_echo())


async def _websocket_headers():
url = "ws://localhost:{}/python-websocket/headerssocket".format(PORT)
@pytest.mark.asyncio
async def test_server_proxy_websocket_headers():
url = "ws://localhost:{}/python-websocket/headerssocket?token={}".format(PORT, TOKEN)
conn = await websocket_connect(url)
await conn.write_message("Hello")
msg = await conn.read_message()
Expand All @@ -276,20 +268,23 @@ async def _websocket_headers():
assert headers['X-Custom-Header'] == 'pytest-23456'


def test_server_proxy_websocket_headers(event_loop):
event_loop.run_until_complete(_websocket_headers())


async def _websocket_subprotocols():
url = "ws://localhost:{}/python-websocket/subprotocolsocket".format(PORT)
@pytest.mark.asyncio
async def test_server_proxy_websocket_subprotocols():
url = "ws://localhost:{}/python-websocket/subprotocolsocket?token={}".format(PORT, TOKEN)
conn = await websocket_connect(url, subprotocols=["protocol_1", "protocol_2"])
await conn.write_message("Hello, world!")
msg = await conn.read_message()
assert json.loads(msg) == ["protocol_1", "protocol_2"]


def test_server_proxy_websocket_subprotocols(event_loop):
event_loop.run_until_complete(_websocket_subprotocols())
@pytest.mark.asyncio
async def test_websocket_no_auth_failure():
# Intentionally do not pass an appropriate token, which should cause a 403
url = "ws://localhost:{}/python-websocket/headerssocket".format(PORT)

with pytest.raises(HTTPClientError, match=r".*HTTP 403: Forbidden.*"):
await websocket_connect(url)


@pytest.mark.parametrize(
"proxy_path, status",
Expand Down

0 comments on commit bead903

Please sign in to comment.