VYPR
High severity7.5NVD Advisory· Published Apr 3, 2024· Updated Apr 15, 2026

CVE-2024-30265

CVE-2024-30265

Description

Collabora Online is a collaborative online office suite based on LibreOffice technology. Any deployment of voilà dashboard allow local file inclusion. Any file on a filesystem that is readable by the user that runs the voilà dashboard server can be downloaded by someone with network access to the server. Whether this still requires authentication depends on how voilà is deployed. This issue has been patched in 0.2.17, 0.3.8, 0.4.4 and 0.5.6.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
voilaPyPI
>= 0.0.2, < 0.2.170.2.17
voilaPyPI
>= 0.3.0a0, < 0.3.80.3.8
voilaPyPI
>= 0.4.0a0, < 0.4.40.4.4
voilaPyPI
>= 0.5.0a0, < 0.5.60.5.6

Patches

5
5542e4ae36bb

Merge pull request from GHSA-2q59-h24c-w6fg

https://github.com/voila-dashboards/voilamartinRenouApr 2, 2024via ghsa
1 file changed · +0 1
  • voila/app.py+0 1 modified
    @@ -661,7 +661,6 @@ def init_settings(self) -> Dict:
                 autoreload=self.autoreload,
                 voila_jinja2_env=env,
                 jinja2_env=server_env,
    -            static_path="/",
                 server_root_dir="/",
                 contents_manager=self.contents_manager,
                 config_manager=self.config_manager,
    
c045be698853

GHSA-2q59-h24c-w6fg

https://github.com/voila-dashboards/voilamartinRenouApr 2, 2024via ghsa
1 file changed · +0 1
  • voila/app.py+0 1 modified
    @@ -446,7 +446,6 @@ def start(self):
                 autoreload=self.autoreload,
                 voila_jinja2_env=env,
                 jinja2_env=env,
    -            static_path='/',
                 server_root_dir='/',
                 contents_manager=self.contents_manager,
                 config_manager=self.config_manager
    
00d6362c237b

GHSA-2q59-h24c-w6fg

https://github.com/voila-dashboards/voilamartinRenouApr 2, 2024via ghsa
1 file changed · +0 1
  • voila/app.py+0 1 modified
    @@ -600,7 +600,6 @@ def init_settings(self) -> Dict:
                 autoreload=self.autoreload,
                 voila_jinja2_env=env,
                 jinja2_env=server_env if JUPYTER_SERVER_2 else env,
    -            static_path='/',
                 server_root_dir='/',
                 contents_manager=self.contents_manager,
                 config_manager=self.config_manager,
    
98b6a40fec27

GHSA-2q59-h24c-w6fg

https://github.com/voila-dashboards/voilamartinRenouApr 2, 2024via ghsa
1 file changed · +0 1
  • voila/app.py+0 1 modified
    @@ -487,7 +487,6 @@ def start(self):
                 autoreload=self.autoreload,
                 voila_jinja2_env=env,
                 jinja2_env=env,
    -            static_path='/',
                 server_root_dir='/',
                 contents_manager=self.contents_manager,
                 config_manager=self.config_manager
    
28faacc9b03b

refactor: reuse voila to be a server extension + tree view + autoreload

https://github.com/voila-dashboards/voilaMaarten BreddelsSep 6, 2018via ghsa
9 files changed · +435 66
  • voila/app.py+53 65 modified
    @@ -7,57 +7,25 @@
     import tempfile
     import json
     import logging
    +import gettext
    +
    +import jinja2
     
     import tornado.ioloop
     import tornado.web
     
    -from pathlib import Path
    -
     from traitlets.config.application import Application
     from traitlets import Unicode, Integer, Bool, default
     
    -from jupyter_server.utils import url_path_join, url_escape
    -from jupyter_server.base.handlers import JupyterHandler
     from jupyter_server.services.kernels.kernelmanager import MappingKernelManager
    -from jupyter_server.services.kernels.handlers import KernelHandler, MainKernelHandler, ZMQChannelsHandler
    -
    -from jupyter_client.jsonutil import date_default
    -
    -import nbformat
    -from nbconvert.preprocessors.execute import executenb
    -from nbconvert import HTMLExporter
    -
    -ROOT = Path(os.path.dirname(__file__))
    -DEFAULT_STATIC_ROOT = ROOT / 'static'
    -TEMPLATE_ROOT = ROOT / 'templates'
    -
    -class VoilaHandler(JupyterHandler):
    -
    -    def initialize(self, notebook=None, strip_sources=False):
    -        self.notebook = notebook
    -        self.strip_sources = strip_sources
    -
    -    @tornado.web.authenticated
    -    @tornado.gen.coroutine
    -    def get(self):
    +from jupyter_server.services.kernels.handlers import KernelHandler, ZMQChannelsHandler
    +from jupyter_server.base.handlers import path_regex
    +from jupyter_server.services.contents.largefilemanager import LargeFileManager
     
    -        # Ignore requested kernel name and make use of the one specified in the notebook.
    -        kernel_name = self.notebook.metadata.get('kernelspec', {}).get('name', self.kernel_manager.default_kernel_name)
    -
    -        # Launch kernel and execute notebook.
    -        kernel_id = yield tornado.gen.maybe_future(self.kernel_manager.start_kernel(kernel_name=kernel_name))
    -        km = self.kernel_manager.get_kernel(kernel_id)
    -        result = executenb(self.notebook, km=km)
    -
    -        # render notebook to html
    -        resources = dict(kernel_id=kernel_id)
    -        html, resources = HTMLExporter(template_file=str(TEMPLATE_ROOT / 'voila.tpl'), exclude_input=self.strip_sources,
    -                                       exclude_output_prompt=self.strip_sources, exclude_input_prompt=self.strip_sources
    -                                      ).from_notebook_node(result, resources=resources)
    -
    -        # Compose reply
    -        self.set_header('Content-Type', 'text/html')
    -        self.write(html)
    +from .paths import ROOT, STATIC_ROOT, TEMPLATE_ROOT
    +from .handler import VoilaHandler
    +from .treehandler import VoilaTreeHandler
    +from .watchdog import WatchDogHandler
     
     _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
     
    @@ -73,7 +41,7 @@ class Voila(Application):
         )
         option_description = Unicode(
             """
    -        notebook_filename:
    +        notebook_path:
                 File name of the Jupyter notebook to display.
             """
         )
    @@ -84,15 +52,21 @@ class Voila(Application):
             config=True,
             help='Port of the voila server. Default 8866.'
         )
    +    autoreload = Bool(
    +        False,
    +        config=True,
    +        help='Will autoreload to server and the page when a template, js file or Python code changes'
    +    )
         static_root = Unicode(
    -        str(DEFAULT_STATIC_ROOT),
    +        str(STATIC_ROOT),
             config=True,
             help='Directory holding static assets (HTML, JS and CSS files).'
         )
         aliases = {
             'port': 'Voila.port',
             'static': 'Voila.static_root',
    -        'strip_sources': 'Voila.strip_sources'
    +        'strip_sources': 'Voila.strip_sources',
    +        'autoreload': 'Voila.autoreload'
         }
         connection_dir_root = Unicode(
             config=True,
    @@ -116,14 +90,7 @@ def _default_log_level(self):
     
         def parse_command_line(self, argv=None):
             super(Voila, self).parse_command_line(argv)
    -        try:
    -            notebook_filename = self.extra_args[0]
    -        except IndexError:
    -            self.log.critical('Bad command line parameters.')
    -            self.log.critical('Missing NOTEBOOK_FILENAME parameter.')
    -            self.log.critical('Run `voila --help` for help on command line parameters.')
    -            exit(1)
    -        self.notebook_filename = notebook_filename
    +        self.notebook_path = self.extra_args[0] if len(self.extra_args) == 1 else None
     
         def start(self):
             connection_dir = tempfile.mkdtemp(
    @@ -143,21 +110,11 @@ def start(self):
                 ]
             )
     
    -        notebook = nbformat.read(self.notebook_filename, as_version=4)
    -
             handlers = [
    -            (
    -                r'/',
    -                VoilaHandler,
    -                {
    -                    'notebook': notebook,
    -                    'strip_sources': self.strip_sources
    -                }
    -            ),
                 (r'/api/kernels/%s' % _kernel_id_regex, KernelHandler),
                 (r'/api/kernels/%s/channels' % _kernel_id_regex, ZMQChannelsHandler),
                 (
    -                r"/static/(.*)",
    +                r"/voila/static/(.*)",
                     tornado.web.StaticFileHandler,
                     {
                         'path': self.static_root,
    @@ -166,10 +123,41 @@ def start(self):
                 )
             ]
     
    +        if self.notebook_path:
    +            handlers.append((
    +                r'/',
    +                VoilaHandler,
    +                {
    +                    'notebook_path': self.notebook_path,
    +                    'strip_sources': self.strip_sources
    +                }
    +            ))
    +        else:
    +            handlers.extend([
    +                ('/', VoilaTreeHandler),
    +                ('/voila/tree' + path_regex, VoilaTreeHandler),
    +                ('/voila/render' + path_regex, VoilaHandler, {'strip_sources': self.strip_sources}),
    +            ])
    +        if self.autoreload:
    +            handlers.append(('/voila/watchdog' + path_regex, WatchDogHandler))
    +
    +        jenv_opt = {"autoescape": True}  # we might want extra options via cmd line like notebook server
    +        env = jinja2.Environment(loader=jinja2.FileSystemLoader(str(TEMPLATE_ROOT)), extensions=['jinja2.ext.i18n'], **jenv_opt)
    +        nbui = gettext.translation('nbui', localedir=str(ROOT / 'i18n'), fallback=True)
    +        env.install_gettext_translations(nbui, newstyle=False)
    +
    +        contents_manager = LargeFileManager()  # TODO: make this configurable like notebook
    +
    +
             app = tornado.web.Application(
                 handlers,
                 kernel_manager=kernel_manager,
    -            allow_remote_access=True
    +            allow_remote_access=True,
    +            autoreload=self.autoreload,
    +            voila_jinja2_env=env,
    +            static_path='/',
    +            server_root_dir='/',
    +            contents_manager=contents_manager
             )
     
             app.listen(self.port)
    
  • voila/handler.py+45 0 added
    @@ -0,0 +1,45 @@
    +import tornado.web
    +
    +from jupyter_server.base.handlers import JupyterHandler
    +
    +import nbformat
    +from nbconvert.preprocessors.execute import executenb
    +from nbconvert import HTMLExporter
    +
    +from .paths import TEMPLATE_ROOT
    +
    +
    +class VoilaHandler(JupyterHandler):
    +    def initialize(self, notebook_path=None, strip_sources=True):
    +        self.notebook_path = notebook_path
    +        self.strip_sources = strip_sources
    +
    +    @tornado.web.authenticated
    +    @tornado.gen.coroutine
    +    def get(self, path=None):
    +        if path:
    +            path = path.strip('/')  # remove leading /
    +            path += '.ipynb'  # when used as a jupyter server extension, we don't use the extension
    +        # if the handler got a notebook_path argument, always serve that
    +        notebook_path = self.notebook_path or path
    +
    +        notebook = nbformat.read(notebook_path, as_version=4)
    +
    +        # Ignore requested kernel name and make use of the one specified in the notebook.
    +        kernel_name = notebook.metadata.get('kernelspec', {}).get('name', self.kernel_manager.default_kernel_name)
    +
    +        # Launch kernel and execute notebook.
    +        kernel_id = yield tornado.gen.maybe_future(self.kernel_manager.start_kernel(kernel_name=kernel_name))
    +        km = self.kernel_manager.get_kernel(kernel_id)
    +        result = executenb(notebook, km=km)
    +
    +        # render notebook to html
    +        resources = dict(kernel_id=kernel_id)
    +        html, resources = HTMLExporter(template_file=str(TEMPLATE_ROOT / 'voila.tpl'), exclude_input=self.strip_sources,
    +                                       exclude_output_prompt=self.strip_sources, exclude_input_prompt=self.strip_sources
    +                                      ).from_notebook_node(result, resources=resources)
    +
    +        # Compose reply
    +        self.set_header('Content-Type', 'text/html')
    +        self.write(html)
    +
    
  • voila/paths.py+7 0 added
    @@ -0,0 +1,7 @@
    +import os
    +from pathlib import Path
    +
    +ROOT = Path(os.path.dirname(__file__))
    +STATIC_ROOT = ROOT / 'static'
    +TEMPLATE_ROOT = ROOT / 'templates'
    +
    
  • voila/server_extension.py+37 0 added
    @@ -0,0 +1,37 @@
    +import os
    +import gettext
    +from pathlib import Path
    +
    +from jinja2 import Environment, FileSystemLoader
    +
    +import tornado.web
    +
    +from jupyter_server.utils import url_path_join
    +from jupyter_server.base.handlers import path_regex
    +
    +from .paths import ROOT, TEMPLATE_ROOT, STATIC_ROOT
    +from .handler import VoilaHandler
    +from .treehandler import VoilaTreeHandler
    +from .watchdog import WatchDogHandler
    +
    +
    +def load_jupyter_server_extension(server_app):
    +    web_app = server_app.web_app
    +
    +    jenv_opt = {"autoescape": True}
    +    env = Environment(loader=FileSystemLoader(str(TEMPLATE_ROOT)), extensions=['jinja2.ext.i18n'], **jenv_opt)
    +    web_app.settings['voila_jinja2_env'] = env
    +
    +    nbui = gettext.translation('nbui', localedir=str(ROOT / 'i18n'), fallback=True)
    +    env.install_gettext_translations(nbui, newstyle=False)
    +
    +    host_pattern = '.*$'
    +    web_app.add_handlers(host_pattern, [
    +        (url_path_join(web_app.settings['base_url'], '/voila/render' + path_regex), VoilaHandler),
    +        (url_path_join(web_app.settings['base_url'], '/voila/watchdog' + path_regex), WatchDogHandler),
    +        (url_path_join(web_app.settings['base_url'], '/voila'), VoilaTreeHandler),
    +        (url_path_join(web_app.settings['base_url'], '/voila/tree' + path_regex), VoilaTreeHandler),
    +        (url_path_join(web_app.settings['base_url'], '/voila/static/(.*)'),  tornado.web.StaticFileHandler,
    +            {'path': str(STATIC_ROOT)})
    +
    +    ])
    \ No newline at end of file
    
  • voila/static/main.js+16 1 modified
    @@ -9,7 +9,7 @@ Array.prototype.forEach.call(scripts, (script) => {
     })
     
     requirejs.config({
    -    baseUrl: 'static/dist'
    +    baseUrl: '/voila/static/dist'
     })
     
     require(['libwidgets'], function(lib) {
    @@ -25,6 +25,21 @@ require(['libwidgets'], function(lib) {
     
         var widgetApp = new lib.WidgetApplication(BASEURL, WSURL, lib.requireLoader, kernel_id);
     
    +    var path = window.location.pathname.substr(14);
    +    var wsWatchdog = new WebSocket(WSURL + '/voila/watchdog/' + path);
    +    wsWatchdog.onmessage = (evt) => {
    +        var msg = JSON.parse(evt.data)
    +        console.log('msg', msg)
    +        if(msg.type == 'reload') {
    +            var timeout = 0;
    +            if(msg.delay == 'long')
    +                timeout = 1000;
    +            setTimeout(() => {
    +                location.href = location.href;
    +            }, timeout)
    +        }
    +    }
    +
         window.addEventListener("beforeunload", function (e) {
             widgetApp.cleanWidgets();
         });
    
  • voila/templates/page.html+96 0 added
    @@ -0,0 +1,96 @@
    +<!DOCTYPE HTML>
    +<html>
    +
    +<head>
    +    <meta charset="utf-8">
    +
    +    <title>{% block title %}Voila{% endblock %}</title>
    +    {% block favicon %}<link id="favicon" rel="shortcut icon" type="image/x-icon" href="{{static_url("base/images/favicon.ico") }}">{% endblock %}
    +    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    +    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    +    
    +    {% block stylesheet %}
    +    <!-- <link rel="stylesheet" href="{{ static_url("style/style.min.css") }}" type="text/css"/> -->
    +    {% endblock %}
    +    <!-- <link rel="stylesheet" href="{{ base_url }}custom/custom.css" type="text/css" /> -->
    +    <link rel="stylesheet" href="https://unpkg.com/font-awesome@4.5.0/css/font-awesome.min.css" type="text/css" />
    +    {% block meta %}
    +    {% endblock %}
    +
    +</head>
    +
    +<body class="{% block bodyclasses %}{% endblock %}"
    + {% block params %}
    +  {% if logged_in and token %}
    +    data-jupyter-api-token="{{token | urlencode}}"
    +  {% endif %}
    + {% endblock params %}
    +dir="ltr">
    +
    +<noscript>
    +    <div id='noscript'>
    +      {% trans %}Voila requires JavaScript.{% endtrans %}<br>
    +      {% trans %}Please enable it to proceed. {% endtrans %}
    +  </div>
    +</noscript>
    +
    +<div id="header">
    +  <div id="header-container" class="container">
    +  {% block login_widget %}
    +
    +    <span id="login_widget">
    +      {% if logged_in %}
    +        <button id="logout" class="btn btn-sm navbar-btn">{% trans %}Logout{% endtrans %}</button>
    +      {% elif login_available and not logged_in %}
    +        <button id="login" class="btn btn-sm navbar-btn">{% trans %}Login{% endtrans %}</button>
    +      {% endif %}
    +    </span>
    +
    +  {% endblock %}
    +  
    +  </div>
    +  <div class="header-bar"></div>
    +
    +  {% block header %}
    +  {% endblock %}
    +</div>
    +
    +<div id="site">
    +{% block site %}
    +{% endblock %}
    +</div>
    +
    +{% block after_site %}
    +{% endblock %}
    +
    +{% block script %}
    +{% endblock %}
    +
    +<script type='text/javascript'>
    +  function _remove_token_from_url() {
    +    if (window.location.search.length <= 1) {
    +      return;
    +    }
    +    var search_parameters = window.location.search.slice(1).split('&');
    +    for (var i = 0; i < search_parameters.length; i++) {
    +      if (search_parameters[i].split('=')[0] === 'token') {
    +        // remote token from search parameters
    +        search_parameters.splice(i, 1);
    +        var new_search = '';
    +        if (search_parameters.length) {
    +          new_search = '?' + search_parameters.join('&');
    +        }
    +        var new_url = window.location.origin + 
    +                      window.location.pathname + 
    +                      new_search + 
    +                      window.location.hash;
    +        window.history.replaceState({}, "", new_url);
    +        return;
    +      }
    +    }
    +  }
    +  _remove_token_from_url();
    +</script>
    +</body>
    +
    +</html>
    
  • voila/templates/tree.html+53 0 added
    @@ -0,0 +1,53 @@
    +{% extends "page.html" %}
    +
    +{% block title %}{{page_title}}{% endblock %}
    +
    +
    +{% block params %}
    +{{super()}}
    +data-base-url="{{base_url | urlencode}}"
    +data-notebook-path="{{notebook_path | urlencode}}"
    +data-terminals-available="{{terminals_available}}"
    +data-server-root="{{server_root}}"
    +{% endblock %}
    +
    +{% block headercontainer %}
    +<span class="flex-spacer"></span>
    +{% endblock %}
    +
    +{% block site %}
    +
    +
    +<!-- <ul class="breadcrumb">
    +  <li><a href="{{breadcrumbs[0][0]}}"><i class="fa fa-folder" alt="folder icon"></i></a></li>
    +{% for crumb in breadcrumbs[1:] %}
    +  <li><a href="{{crumb[0]}}">{{crumb[1]}}</a></li>
    +{% endfor %}
    +</ul>
    + -->
    +
    +<ul class="voila-notebooks">
    +  {% if breadcrumbs|length > 1: %}
    +    <li><a href="{{breadcrumbs[-2][0]}}"><i class="fa fa-folder"></i>..</a></li>
    +  {% endif %}
    +
    +  {% for content in contents.content %}
    +    {% if content.type == 'notebook' %}
    +      <li><a href="/voila/render/{{content.path.rpartition('.')[0]}}"><i class="fa fa-book"></i>{{content.name.rpartition('.')[0]}}</a></li>
    +    {% endif %}
    +    {% if content.type == 'directory' %}
    +      <li><a href="/voila/tree/{{content.path}}"><i class="fa fa-folder"></i>{{content.name}}</a></li>
    +    {% endif %}
    +  {% endfor %}
    +</ul>
    +
    +
    +
    +
    +{% endblock %}
    +
    +{% block script %}
    +    {{super()}}
    +{% endblock %}
    +
    +
    
  • voila/treehandler.py+60 0 added
    @@ -0,0 +1,60 @@
    +from jupyter_server.base.handlers import JupyterHandler
    +from jupyter_server.utils import url_path_join, url_escape
    +
    +class VoilaTreeHandler(JupyterHandler):
    +    def get_template(self, name):
    +        """Return the jinja template object for a given name"""
    +        return self.settings['voila_jinja2_env'].get_template(name)
    +
    +    def generate_breadcrumbs(self, path):
    +        breadcrumbs = [(url_path_join(self.base_url, 'voila/tree'), '')]
    +        parts = path.split('/')
    +        for i in range(len(parts)):
    +            if parts[i]:
    +                link = url_path_join(self.base_url, 'voila/tree',
    +                    url_escape(url_path_join(*parts[:i+1])),
    +                )
    +                breadcrumbs.append((link, parts[i]))
    +        return breadcrumbs
    +
    +    def generate_page_title(self, path):
    +        parts = path.split('/')
    +        if len(parts) > 3:  # not too many parts
    +            parts = parts[-2:]
    +        page_title = url_path_join(*parts)
    +        if page_title:
    +            return page_title+'/'
    +        else:
    +            return 'Voila Home'
    +
    +    def get(self, path=''):
    +        cm = self.contents_manager
    +        print(path, cm)
    +
    +        if cm.dir_exists(path=path):
    +            if cm.is_hidden(path) and not cm.allow_hidden:
    +                self.log.info("Refusing to serve hidden directory, via 404 Error")
    +                raise web.HTTPError(404)
    +            breadcrumbs = self.generate_breadcrumbs(path)
    +            page_title = self.generate_page_title(path)
    +            contents = cm.get(path)
    +            self.write(self.render_template('tree.html',
    +                page_title=page_title,
    +                notebook_path=path,
    +                breadcrumbs=breadcrumbs,
    +                contents=contents,
    +                terminals_available=False,
    +                server_root=self.settings['server_root_dir'],
    +            ))
    +        elif cm.file_exists(path):
    +            # it's not a directory, we have redirecting to do
    +            model = cm.get(path, content=False)
    +            # redirect to /api/notebooks if it's a notebook, otherwise /api/files
    +            service = 'notebooks' if model['type'] == 'notebook' else 'files'
    +            url = url_path_join(
    +                self.base_url, service, url_escape(path),
    +            )
    +            self.log.debug("Redirecting %s to %s", self.request.path, url)
    +            self.redirect(url)
    +        else:
    +            raise web.HTTPError(404)
    
  • voila/watchdog.py+68 0 added
    @@ -0,0 +1,68 @@
    +import asyncio
    +
    +import watchdog.events
    +import watchdog.observers
    +
    +import tornado.websocket
    +import tornado.ioloop
    +
    +from .paths import ROOT
    +
    +# we cache event handler for watchdogs not to waste resources
    +event_handlers = {}
    +
    +class WatchDogEventHandler(watchdog.events.RegexMatchingEventHandler):
    +    def __init__(self, *args, **kwargs):
    +        super(WatchDogEventHandler, self).__init__(*args, **kwargs)
    +        self.listeners = []
    +
    +    def on_any_event(self, event):
    +        try:
    +            asyncio.get_event_loop()
    +        except RuntimeError:
    +            asyncio.set_event_loop(asyncio.new_event_loop())
    +        for listener in self.listeners:
    +            listener()
    +
    +
    +
    +class WatchDogHandler(tornado.websocket.WebSocketHandler):
    +    #@tornado.gen.coroutine
    +    def open(self, path=''):
    +        self.callback = tornado.ioloop.PeriodicCallback(lambda: self.ping(''), 6000)
    +        path = path.strip('/') + '.ipynb'
    +        if path not in event_handlers:
    +            watchdog_observer = watchdog.observers.Observer()
    +            # sometimes useful to add this when triggering does not work
    +            # from watchdog.events import LoggingEventHandler
    +            # logging_handler = LoggingEventHandler()
    +            # watchdog_observer.schedule(logging_handler, '.', recursive=True)
    +            
    +            handler = WatchDogEventHandler(regexes=['\\./' + path])
    +            watchdog_observer.schedule(handler, '.', recursive=True)
    +            
    +            handler = WatchDogEventHandler(regexes=[str(ROOT) +r'/templates/.*', str(ROOT / 'static/main.js'), str(ROOT / 'static/dist/libwidgets.js')])
    +            watchdog_observer.schedule(handler, str(ROOT), recursive=True)
    +            
    +            watchdog_observer.start()
    +            event_handlers[path] = handler
    +            
    +            tornado.autoreload.add_reload_hook(self._on_reload)
    +        self.handler = event_handlers[path]
    +        self.handler.listeners.append(self)
    +
    +    def __call__(self):
    +        self.write_message({'type': 'reload', 'delay': 'no'})
    +
    +    def on_close(self):
    +        self.handler.listeners.remove(self)
    +
    +    def _on_reload(self):
    +        try:
    +            self.write_message({'type': 'reload', 'delay': 'long'})
    +        except tornado.websocket.WebSocketClosedError:
    +            print('a websocket was already closed')
    +
    +
    +    def check_origin(self, origin):
    +        return True
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

8

News mentions

0

No linked articles in our index yet.