diff --git a/docs/news.rst b/docs/news.rst index d2620901..044d8cfc 100644 --- a/docs/news.rst +++ b/docs/news.rst @@ -60,7 +60,12 @@ Fixed - The :ref:`schedule.json` webservice sets the ``node_name`` field in error responses. - The next pending job for all but one project was unreported by the :ref:`daemonstatus.json` and :ref:`listjobs.json` webservices, and was not cancellable by the :ref:`cancel.json` webservice. - Restore support for :ref:`eggstorage` implementations whose ``get()`` methods return file-like objects without ``name`` attributes (1.4.3 regression). + +Security +^^^^^^^^ + - The ``FilesystemEggStorage`` class used by the :ref:`listversions.json` webservice escapes project names before globbing, to disallow listing arbitrary directories. +- The :ref:`webui` escapes user input (project names, spider names, and job IDs) to prevent cross-site scripting (XSS). Platform support ^^^^^^^^^^^^^^^^ diff --git a/scrapyd/website.py b/scrapyd/website.py index 7a60120e..66472f4f 100644 --- a/scrapyd/website.py +++ b/scrapyd/website.py @@ -203,7 +203,7 @@ def render_GET(self, txrequest): if self.root.scheduler.list_projects(): s += '

Available projects:

\n

\n' else: s += '

No projects available.

\n' @@ -235,6 +235,16 @@ def microsec_trunc(timelike): return timelike - timedelta(microseconds=ms) +def cancel_button(project, jobid, base_path): + return f""" +
+ + + +
+ """ + + class Jobs(PrefixHeaderMixin, resource.Resource): def __init__(self, root, local_items): @@ -243,14 +253,6 @@ def __init__(self, root, local_items): self.local_items = local_items self.prefix_header = root.prefix_header - cancel_button = """ -
- - - -
- """.format - header_cols = [ 'Project', 'Spider', 'Job', 'PID', @@ -316,9 +318,9 @@ def prep_table(self): def prep_tab_pending(self): return '\n'.join( self.prep_row({ - "Project": project, - "Spider": m['name'], - "Job": m['_job'], + "Project": escape(project), + "Spider": escape(m['name']), + "Job": escape(m['_job']), "Cancel": self.cancel_button(project=project, jobid=m['_job'], base_path=self.base_path), }) for project, queue in self.root.poller.queues.items() @@ -328,9 +330,9 @@ def prep_tab_pending(self): def prep_tab_running(self): return '\n'.join( self.prep_row({ - "Project": p.project, - "Spider": p.spider, - "Job": p.job, + "Project": escape(p.project), + "Spider": escape(p.spider), + "Job": escape(p.job), "PID": p.pid, "Start": microsec_trunc(p.start_time), "Runtime": microsec_trunc(datetime.now() - p.start_time), @@ -344,9 +346,9 @@ def prep_tab_running(self): def prep_tab_finished(self): return '\n'.join( self.prep_row({ - "Project": p.project, - "Spider": p.spider, - "Job": p.job, + "Project": escape(p.project), + "Spider": escape(p.spider), + "Job": escape(p.job), "Start": microsec_trunc(p.start_time), "Runtime": microsec_trunc(p.end_time - p.start_time), "Finish": microsec_trunc(p.end_time),