Skip to content

Commit

Permalink
fix: Escape user input on home and jobs pages
Browse files Browse the repository at this point in the history
  • Loading branch information
jpmckinney committed Jul 17, 2024
1 parent d346503 commit 848fc18
Show file tree
Hide file tree
Showing 2 changed files with 25 additions and 18 deletions.
5 changes: 5 additions & 0 deletions docs/news.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
^^^^^^^^^^^^^^^^
Expand Down
38 changes: 20 additions & 18 deletions scrapyd/website.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ def render_GET(self, txrequest):
if self.root.scheduler.list_projects():
s += '<p>Available projects:<p>\n<ul>\n'
for project_name in sorted(self.root.scheduler.list_projects()):
s += f'<li>{project_name}</li>\n'
s += f'<li>{escape(project_name)}</li>\n'
s += '</ul>\n'
else:
s += '<p>No projects available.</p>\n'
Expand Down Expand Up @@ -235,6 +235,16 @@ def microsec_trunc(timelike):
return timelike - timedelta(microseconds=ms)


def cancel_button(project, jobid, base_path):
return f"""
<form method="post" onsubmit="return confirm('Are you sure?');" action="{base_path}/cancel.json">
<input type="hidden" name="project" value="{escape(project)}"/>
<input type="hidden" name="job" value="{escape(jobid)}"/>
<input type="submit" style="float: left;" value="Cancel"/>
</form>
"""


class Jobs(PrefixHeaderMixin, resource.Resource):

def __init__(self, root, local_items):
Expand All @@ -243,14 +253,6 @@ def __init__(self, root, local_items):
self.local_items = local_items
self.prefix_header = root.prefix_header

cancel_button = """
<form method="post" onsubmit="return confirm('Are you sure?');" action="{base_path}/cancel.json">
<input type="hidden" name="project" value="{project}"/>
<input type="hidden" name="job" value="{jobid}"/>
<input type="submit" style="float: left;" value="Cancel"/>
</form>
""".format

header_cols = [
'Project', 'Spider',
'Job', 'PID',
Expand Down Expand Up @@ -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()
Expand All @@ -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),
Expand All @@ -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),
Expand Down

0 comments on commit 848fc18

Please sign in to comment.