From 3ef5fbd602c2343c7e12130586d4c8a127988620 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 11 Aug 2026 19:10:47 -0700 Subject: [PATCH 1/3] Add sanitization and HTML escape --- cgi-bin/pyupload.cgi | 84 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 69 insertions(+), 15 deletions(-) diff --git a/cgi-bin/pyupload.cgi b/cgi-bin/pyupload.cgi index 27770e6..35fcd71 100755 --- a/cgi-bin/pyupload.cgi +++ b/cgi-bin/pyupload.cgi @@ -1,18 +1,22 @@ #!/usr/bin/env python import cgitb + cgitb.enable() import cgi import logging import os +import html import socket +import pathlib +import sys -REQUEST_METHOD = os.environ.get('REQUEST_METHOD', 'GET') -REQUEST_PORT = os.environ.get("SERVER_PORT", '8000') +REQUEST_METHOD = os.environ.get("REQUEST_METHOD", "GET") +REQUEST_PORT = os.environ.get("SERVER_PORT", "8000") -def render_form(success_filenames=None): +def render_form(success_filenames=None, errors=None): print("Content-Type: text/html") print() print(""" @@ -27,7 +31,16 @@ def render_form(success_filenames=None):
") + + if errors: + print(""" +

Files not uploaded successfully:

+
") print(f""" @@ -55,21 +68,62 @@ def render_form(success_filenames=None): """) except ImportError: - logging.warning('Skipping generating address QR code, qrcode library not installed or not in python path.') + logging.warning( + "Skipping generating address QR code, qrcode library not installed or not in python path." + ) -if REQUEST_METHOD == 'POST': - form = cgi.FieldStorage() - files = form['uploadedfile'] if isinstance(form['uploadedfile'], list) else [form['uploadedfile']] +class PyUploadError(ValueError): + pass - filenames = [] + +def sanitize_path(path, required_parent_dir, disallowed_dirs): + test_results = [] + target = (pathlib.Path(required_parent_dir) / path).resolve() + for test_dir in (required_parent_dir, *disallowed_dirs): + base = pathlib.Path(test_dir).resolve() + test_results.append(target != base and target.is_relative_to(base)) + + if not test_results[0] or any(test_results[1:]): + raise PyUploadError("Invalid path") + + return target.absolute() + + +if REQUEST_METHOD == "POST": + form = cgi.FieldStorage() + files = ( + form["uploadedfile"] + if isinstance(form["uploadedfile"], list) + else [form["uploadedfile"]] + ) + + success_filenames = [] + errors = [] for file in files: filename = file.filename - filenames.append(filename) - with open(f'{os.path.dirname(__file__)}/../{filename}', 'wb+') as f: - f.write(file.file.read()) - - render_form(success_filenames=filenames) + try: + if not filename: + raise PyUploadError("No filename") + + base_dir = f"{os.path.dirname(__file__)}/../" + upload_path = sanitize_path( + filename, + base_dir, + [f"{base_dir}cgi-bin", f"{base_dir}htbin", f"{base_dir}.git"], + ) + print(upload_path) + with open(upload_path, "wb+") as f: + f.write(file.file.read()) + + success_filenames.append(filename) + except PyUploadError as e: + errors.append((filename, e.args[0])) + except Exception as e: + print("Failed to upload", e, file=sys.stderr) + errors.append((filename, "Internal error")) + + render_form(success_filenames=success_filenames, errors=errors) else: - render_form() \ No newline at end of file + render_form() From ca6d622726b872192139aed97aaf32ce49a076d9 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 11 Aug 2026 19:19:41 -0700 Subject: [PATCH 2/3] Switch to SVGPathImage --- cgi-bin/pyupload.cgi | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/cgi-bin/pyupload.cgi b/cgi-bin/pyupload.cgi index 35fcd71..1557c37 100755 --- a/cgi-bin/pyupload.cgi +++ b/cgi-bin/pyupload.cgi @@ -1,7 +1,6 @@ #!/usr/bin/env python import cgitb - cgitb.enable() import cgi @@ -60,7 +59,7 @@ def render_form(success_filenames=None, errors=None): import qrcode.image.svg url = f'http://{socket.getfqdn()}.local:{REQUEST_PORT}/cgi-bin/pyupload.cgi' - qr = qrcode.make(url, image_factory=qrcode.image.svg.SvgImage) + qr = qrcode.make(url, image_factory=qrcode.image.svg.SvgPathImage) print(f"""

Scan to access on another device: {url}

@@ -68,9 +67,7 @@ def render_form(success_filenames=None, errors=None): """) except ImportError: - logging.warning( - "Skipping generating address QR code, qrcode library not installed or not in python path." - ) + logging.warning("Skipping generating address QR code, qrcode library not installed or not in python path.") class PyUploadError(ValueError): From 7bfc44b144de7ccf3f2fbf12ad2994cbd5ed556a Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 11 Aug 2026 19:22:01 -0700 Subject: [PATCH 3/3] Clean up --- cgi-bin/pyupload.cgi | 78 ++++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 42 deletions(-) diff --git a/cgi-bin/pyupload.cgi b/cgi-bin/pyupload.cgi index 1557c37..4bf150b 100755 --- a/cgi-bin/pyupload.cgi +++ b/cgi-bin/pyupload.cgi @@ -4,18 +4,17 @@ import cgitb cgitb.enable() import cgi +import html import logging import os -import html -import socket import pathlib -import sys +import socket -REQUEST_METHOD = os.environ.get("REQUEST_METHOD", "GET") -REQUEST_PORT = os.environ.get("SERVER_PORT", "8000") +REQUEST_METHOD = os.environ.get('REQUEST_METHOD', 'GET') +REQUEST_PORT = os.environ.get("SERVER_PORT", '8000') -def render_form(success_filenames=None, errors=None): +def render_form(uploaded=None, not_uploaded=None): print("Content-Type: text/html") print() print(""" @@ -24,22 +23,22 @@ def render_form(success_filenames=None, errors=None): """) - if success_filenames: + if uploaded: print("""

Files uploaded successfully:

    """) - for fname in success_filenames: + for fname in uploaded: print(f"
  • {html.escape(fname)}
  • ") print("

") - if errors: + if not_uploaded: print("""

Files not uploaded successfully:

    """) - for fname, failure_reason in errors: - print(f"
  • {html.escape(fname)} - {html.escape(failure_reason)}
  • ") + for fname, reason in not_uploaded: + print(f"
  • {html.escape(fname)} - {html.escape(reason)}
  • ") print("

") print(f""" @@ -67,60 +66,55 @@ def render_form(success_filenames=None, errors=None): """) except ImportError: - logging.warning("Skipping generating address QR code, qrcode library not installed or not in python path.") + logging.warning('Skipping generating address QR code, qrcode library not installed or not in python path.') -class PyUploadError(ValueError): - pass +def sanitize_path(filename): + if not filename: + raise ValueError("No filename") + required_parent_dir = f"{os.path.dirname(__file__)}/../" + disallowed_dirs = [ + f"{required_parent_dir}cgi-bin", + f"{required_parent_dir}htbin", + f"{required_parent_dir}.git", + ] -def sanitize_path(path, required_parent_dir, disallowed_dirs): test_results = [] - target = (pathlib.Path(required_parent_dir) / path).resolve() + target = (pathlib.Path(required_parent_dir) / filename).resolve() for test_dir in (required_parent_dir, *disallowed_dirs): base = pathlib.Path(test_dir).resolve() test_results.append(target != base and target.is_relative_to(base)) if not test_results[0] or any(test_results[1:]): - raise PyUploadError("Invalid path") + raise ValueError("Invalid filename") return target.absolute() if REQUEST_METHOD == "POST": form = cgi.FieldStorage() - files = ( - form["uploadedfile"] - if isinstance(form["uploadedfile"], list) - else [form["uploadedfile"]] - ) - - success_filenames = [] - errors = [] + files = form['uploadedfile'] if isinstance(form['uploadedfile'], list) else [form['uploadedfile']] + + uploaded = [] + not_uploaded = [] for file in files: filename = file.filename try: - if not filename: - raise PyUploadError("No filename") - - base_dir = f"{os.path.dirname(__file__)}/../" - upload_path = sanitize_path( - filename, - base_dir, - [f"{base_dir}cgi-bin", f"{base_dir}htbin", f"{base_dir}.git"], - ) - print(upload_path) + upload_path = sanitize_path(filename) + logging.info(f"Uploading to '{upload_path}'") + with open(upload_path, "wb+") as f: f.write(file.file.read()) - success_filenames.append(filename) - except PyUploadError as e: - errors.append((filename, e.args[0])) + uploaded.append(filename) + except ValueError as e: + not_uploaded.append((filename, e.args[0])) except Exception as e: - print("Failed to upload", e, file=sys.stderr) - errors.append((filename, "Internal error")) + logging.error("Failed to upload", exc_info=True) + not_uploaded.append((filename, "Internal error")) - render_form(success_filenames=success_filenames, errors=errors) + render_form(uploaded=uploaded, not_uploaded=not_uploaded) else: - render_form() + render_form() \ No newline at end of file