Static analysis fixes for Python codebase (#1277)

* Don't log names of internal symbols that can be used for attacks

* Add integrity check to dropzone.js library

* Better a11y of web form labels

* Safer handling of file download paths

* Don't invert boolean check

* Make backend auth check a flask abort

* Clean up indentation to remove unwanted tabs

* Run workflow either on PR events, or branch pushes, not both
This commit is contained in:
Daniel Markstedt 2023-11-01 22:28:53 +09:00 committed by GitHub
parent 37b9110c99
commit 063e8ed32b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 79 additions and 61 deletions

View File

@ -3,11 +3,9 @@ name: Web Tests/Analysis
on: on:
workflow_dispatch: workflow_dispatch:
push: push:
paths: branches:
- 'python/web/**' - 'develop'
- 'python/common/**' - 'main'
- '.github/workflows/web.yml'
- 'easyinstall.sh'
pull_request: pull_request:
paths: paths:
- 'python/web/**' - 'python/web/**'
@ -19,9 +17,6 @@ on:
- opened - opened
- synchronize - synchronize
- reopened - reopened
branches:
- 'develop'
- 'main'
jobs: jobs:
backend_checks: backend_checks:

View File

@ -64,7 +64,7 @@ class FileCmds:
excluded_dirs = ("Network Trash Folder", "Temporary Items", "TheVolumeSettingsFolder") excluded_dirs = ("Network Trash Folder", "Temporary Items", "TheVolumeSettingsFolder")
for root, dirs, _files in walk(directory, topdown=True): for root, dirs, _files in walk(directory, topdown=True):
# Strip out dirs that begin with . # Strip out dirs that begin with .
dirs[:] = [d for d in dirs if not d[0] == "."] dirs[:] = [d for d in dirs if d[0] != "."]
for dir in dirs: for dir in dirs:
if dir not in excluded_dirs: if dir not in excluded_dirs:
dirpath = path.join(root, dir) dirpath = path.join(root, dir)
@ -488,12 +488,12 @@ class FileCmds:
iso_filename = Path(server_info["image_dir"]) / f"{file_name}.iso" iso_filename = Path(server_info["image_dir"]) / f"{file_name}.iso"
with TemporaryDirectory() as tmp_dir: with TemporaryDirectory() as tmp_dir:
req_proc = self.download_to_dir(quote(url, safe=URL_SAFE), tmp_dir, file_name) tmp_full_path = Path(tmp_dir) / file_name
req_proc = self.download_to_dir(quote(url, safe=URL_SAFE), tmp_full_path)
logging.info("Downloaded %s to %s", file_name, tmp_dir) logging.info("Downloaded %s to %s", file_name, tmp_dir)
if not req_proc["status"]: if not req_proc["status"]:
return {"status": False, "msg": req_proc["msg"]} return {"status": False, "msg": req_proc["msg"]}
tmp_full_path = Path(tmp_dir) / file_name
if is_zipfile(tmp_full_path): if is_zipfile(tmp_full_path):
if "XtraStuf.mac" in str(ZipFile(str(tmp_full_path)).namelist()): if "XtraStuf.mac" in str(ZipFile(str(tmp_full_path)).namelist()):
logging.info( logging.info(
@ -565,9 +565,9 @@ class FileCmds:
} }
# noinspection PyMethodMayBeStatic # noinspection PyMethodMayBeStatic
def download_to_dir(self, url, save_dir, file_name): def download_to_dir(self, url, target_path):
""" """
Takes (str) url, (str) save_dir, (str) file_name Takes (str) url, (Path) target_path
Returns (dict) with (bool) status and (str) msg Returns (dict) with (bool) status and (str) msg
""" """
logging.info("Making a request to download %s", url) logging.info("Making a request to download %s", url)
@ -580,7 +580,7 @@ class FileCmds:
) as req: ) as req:
req.raise_for_status() req.raise_for_status()
try: try:
with open(f"{save_dir}/{file_name}", "wb") as download: with open(str(target_path), "wb") as download:
for chunk in req.iter_content(chunk_size=8192): for chunk in req.iter_content(chunk_size=8192):
download.write(chunk) download.write(chunk)
except FileNotFoundError as error: except FileNotFoundError as error:
@ -593,7 +593,7 @@ class FileCmds:
logging.info("Response content-type: %s", req.headers["content-type"]) logging.info("Response content-type: %s", req.headers["content-type"])
logging.info("Response status code: %s", req.status_code) logging.info("Response status code: %s", req.status_code)
parameters = {"file_name": file_name, "save_dir": save_dir} parameters = {"target_path": str(target_path)}
return { return {
"status": True, "status": True,
"return_code": ReturnCodes.DOWNLOADTODIR_SUCCESS, "return_code": ReturnCodes.DOWNLOADTODIR_SUCCESS,

View File

@ -107,8 +107,7 @@ class CtrlBoardMenuUpdateEventHandler(Observer):
except AttributeError: except AttributeError:
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.error( log.error(
"Handler function [%s] not found or returned an error. Skipping.", "Handler function not found or returned an error. Skipping.",
str(handler_function_name),
) )
# noinspection PyUnusedLocal # noinspection PyUnusedLocal

View File

@ -8,7 +8,7 @@ from piscsi.piscsi_cmds import PiscsiCmds
class CtrlBoardMenuBuilder(MenuBuilder): class CtrlBoardMenuBuilder(MenuBuilder):
"""Class fgor building the control board UI specific menus""" """Class for building the control board UI specific menus"""
SCSI_ID_MENU = "scsi_id_menu" SCSI_ID_MENU = "scsi_id_menu"
ACTION_MENU = "action_menu" ACTION_MENU = "action_menu"
@ -48,7 +48,7 @@ class CtrlBoardMenuBuilder(MenuBuilder):
return self.create_device_info_menu(context_object) return self.create_device_info_menu(context_object)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.warning("Provided menu name [%s] cannot be built!", name) log.error("Provided menu name cannot be built!")
return self.create_scsi_id_list_menu(context_object) return self.create_scsi_id_list_menu(context_object)

View File

@ -23,7 +23,7 @@ class ReturnCodeMapper:
ReturnCodes.DOWNLOADFILETOISO_SUCCESS: ReturnCodes.DOWNLOADFILETOISO_SUCCESS:
_("Created CD-ROM ISO image with arguments \"%(value)s\""), _("Created CD-ROM ISO image with arguments \"%(value)s\""),
ReturnCodes.DOWNLOADTODIR_SUCCESS: ReturnCodes.DOWNLOADTODIR_SUCCESS:
_("%(file_name)s downloaded to %(save_dir)s"), _("Downloaded file to %(target_path)s"),
ReturnCodes.WRITEFILE_SUCCESS: ReturnCodes.WRITEFILE_SUCCESS:
_("File created: %(target_path)s"), _("File created: %(target_path)s"),
ReturnCodes.WRITEFILE_COULD_NOT_WRITE: ReturnCodes.WRITEFILE_COULD_NOT_WRITE:

View File

@ -203,3 +203,7 @@ div.throttle-notice > div a {
div.throttle-notice > div a:hover { div.throttle-notice > div a:hover {
text-decoration: underline; text-decoration: underline;
} }
label.hidden {
display: none;
}

View File

@ -62,6 +62,10 @@ div.notice {
color: #fff; color: #fff;
} }
label.hidden {
display: none;
}
/* /*
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
Tables Tables

View File

@ -396,6 +396,7 @@
<input name="url" id="download_url" required="" type="url"> <input name="url" id="download_url" required="" type="url">
<input type="radio" name="destination" id="disk_images" value="disk_images" checked="checked"> <input type="radio" name="destination" id="disk_images" value="disk_images" checked="checked">
<label for="disk_images">{{ _("Disk Images") }}</label> <label for="disk_images">{{ _("Disk Images") }}</label>
<label for="images_subdir" class="hidden">{{ _("Directory") }}</label>
<select name="images_subdir" id="images_subdir"> <select name="images_subdir" id="images_subdir">
{% for dir in images_subdirs %} {% for dir in images_subdirs %}
<option value="{{dir}}">{{dir}}</option> <option value="{{dir}}">{{dir}}</option>
@ -405,6 +406,7 @@
{% if file_server_dir_exists %} {% if file_server_dir_exists %}
<input type="radio" name="destination" id="shared_files" value="shared_files"> <input type="radio" name="destination" id="shared_files" value="shared_files">
<label for="shared_files">{{ _("Shared Files") }}</label> <label for="shared_files">{{ _("Shared Files") }}</label>
<label for="shared_subdir" class="hidden">{{ _("Directory") }}</label>
<select name="shared_subdir" id="shared_subdir"> <select name="shared_subdir" id="shared_subdir">
{% for dir in shared_subdirs %} {% for dir in shared_subdirs %}
<option value="{{dir}}">{{dir}}</option> <option value="{{dir}}">{{dir}}</option>

View File

@ -11,10 +11,12 @@
<li>{{ _("PiSCSI Config") }} = {{ CFG_DIR }}</li> <li>{{ _("PiSCSI Config") }} = {{ CFG_DIR }}</li>
</ul> </ul>
<h3>{{ _("Destination") }}</h3>
<form name="dropper" action="/files/upload" method="post" class="dropzone dz-clickable" enctype="multipart/form-data" id="dropper"> <form name="dropper" action="/files/upload" method="post" class="dropzone dz-clickable" enctype="multipart/form-data" id="dropper">
<fieldset>
<legend>{{ _("Destination") }}</legend>
<input type="radio" name="destination" id="disk_images" value="disk_images" checked="checked"> <input type="radio" name="destination" id="disk_images" value="disk_images" checked="checked">
<label for="disk_images">{{ _("Disk Images") }}</label> <label for="disk_images">{{ _("Disk Images") }}</label>
<label for="images_subdir" class="hidden">{{ _("Directory") }}</label>
<select name="images_subdir" id="images_subdir"> <select name="images_subdir" id="images_subdir">
{% for dir in images_subdirs %} {% for dir in images_subdirs %}
<option value="{{dir}}">{{dir}}</option> <option value="{{dir}}">{{dir}}</option>
@ -24,6 +26,7 @@
{% if file_server_dir_exists %} {% if file_server_dir_exists %}
<input type="radio" name="destination" id="shared_files" value="shared_files"> <input type="radio" name="destination" id="shared_files" value="shared_files">
<label for="shared_files">{{ _("Shared Files") }}</label> <label for="shared_files">{{ _("Shared Files") }}</label>
<label for="shared_subdir" class="hidden">{{ _("Directory") }}</label>
<select name="shared_subdir" id="shared_subdir"> <select name="shared_subdir" id="shared_subdir">
{% for dir in shared_subdirs %} {% for dir in shared_subdirs %}
<option value="{{dir}}">{{dir}}</option> <option value="{{dir}}">{{dir}}</option>
@ -33,9 +36,15 @@
{% endif %} {% endif %}
<input type="radio" name="destination" id="piscsi_config" value="piscsi_config"> <input type="radio" name="destination" id="piscsi_config" value="piscsi_config">
<label for="piscsi_config">{{ _("PiSCSI Config") }}</label> <label for="piscsi_config">{{ _("PiSCSI Config") }}</label>
</fieldset>
</form> </form>
<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.9.3/min/dropzone.min.js"></script> <script
type="application/javascript"
src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.9.3/min/dropzone.min.js"
integrity="sha384-PwiT+fWTPpIySx6DrH1FKraKo+LvVpOClsjx0TSdMYTKi7BR1hR149f4VHLUUnfA"
crossorigin="anonymous"
></script>
<script type="application/javascript"> <script type="application/javascript">
Dropzone.options.dropper = { Dropzone.options.dropper = {

View File

@ -27,6 +27,7 @@ from flask import (
make_response, make_response,
session, session,
jsonify, jsonify,
abort,
) )
from piscsi.piscsi_cmds import PiscsiCmds from piscsi.piscsi_cmds import PiscsiCmds
@ -991,7 +992,7 @@ def download_file():
else: else:
return response(error=True, message=_("Unknown destination")) return response(error=True, message=_("Unknown destination"))
process = file_cmd.download_to_dir(url, destination_dir, Path(url).name) process = file_cmd.download_to_dir(url, Path(destination_dir) / Path(url).name)
process = ReturnCodeMapper.add_msg(process) process = ReturnCodeMapper.add_msg(process)
if process["status"]: if process["status"]:
return response(message=process["msg"]) return response(message=process["msg"])
@ -1447,6 +1448,16 @@ def log_http_request():
logging.debug(message) logging.debug(message)
@APP.before_request
def check_backend_auth():
if not piscsi_cmd.is_token_auth()["status"] and not APP.config["PISCSI_TOKEN"]:
abort(
403,
"PiSCSI is password protected. "
"Start the Web Interface with the --password parameter.",
)
if __name__ == "__main__": if __name__ == "__main__":
APP.secret_key = "piscsi_is_awesome_insecure_secret_key" APP.secret_key = "piscsi_is_awesome_insecure_secret_key"
APP.config["SESSION_TYPE"] = "filesystem" APP.config["SESSION_TYPE"] = "filesystem"
@ -1524,12 +1535,6 @@ if __name__ == "__main__":
file_cmd = FileCmds(piscsi=piscsi_cmd) file_cmd = FileCmds(piscsi=piscsi_cmd)
sys_cmd = SysCmds() sys_cmd = SysCmds()
if not piscsi_cmd.is_token_auth()["status"] and not APP.config["PISCSI_TOKEN"]:
raise Exception(
"PiSCSI is password protected. "
"Start the Web Interface with the --password parameter."
)
if Path(f"{CFG_DIR}/{DEFAULT_CONFIG}").is_file(): if Path(f"{CFG_DIR}/{DEFAULT_CONFIG}").is_file():
file_cmd.read_config(DEFAULT_CONFIG) file_cmd.read_config(DEFAULT_CONFIG)
if Path(f"{DRIVE_PROPERTIES_FILE}").is_file(): if Path(f"{DRIVE_PROPERTIES_FILE}").is_file():

View File

@ -362,7 +362,7 @@ def test_download_url_to_dir(env, httpserver, http_client, list_files, delete_fi
assert file_name in list_files() assert file_name in list_files()
assert ( assert (
response_data["messages"][0]["message"] response_data["messages"][0]["message"]
== f"{file_name} downloaded to {env['images_dir']}{subdir}" == f"Downloaded file to {env['images_dir']}{subdir}{file_name}"
) )
# Cleanup # Cleanup