diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index d426f5eda..f5f3fe65e 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -174,15 +174,24 @@ def _mime_for_path(path: str) -> str: return mime or "application/octet-stream" +def _text_response(code: HTTPStatus, text: str) -> Response: + """Return an error message. + + Response is returned as text/plain, so no escaping of untrusted + input is required.""" + resp = flask.make_response(text, code) + resp.headers["Content-type"] = "text/plain" + return resp + + def _handle_local_file_request(request: LocalFileRequest) -> Response: directory = request.root path = request.path try: isdir = os.path.isdir(os.path.join(directory, path)) except ValueError: - return flask.make_response( - f"Path for '{directory} - {path}' is too long!", - HTTPStatus.BAD_REQUEST, + return _text_response( + HTTPStatus.BAD_REQUEST, f"Path for '{directory} - {path}' is too long!" ) directory = os.path.realpath(directory) @@ -191,15 +200,14 @@ def _handle_local_file_request(request: LocalFileRequest) -> Response: # protect against directory transversal: https://security.openstack.org/guidelines/dg_using-file-paths.html if not fullpath.startswith(directory): - return flask.make_response( - f"Path for '{directory} - {path}' is a security leak!", - HTTPStatus.FORBIDDEN, + return _text_response( + HTTPStatus.FORBIDDEN, f"Path for '{directory} - {path}' is a security leak!" ) if isdir: - return flask.make_response( - f"Path for '{directory} - {path}' is a directory (not supported)!", + return _text_response( HTTPStatus.FORBIDDEN, + f"Path for '{directory} - {path}' is a directory (not supported)!", ) try: @@ -219,10 +227,7 @@ def _handle_local_file_request(request: LocalFileRequest) -> Response: ) else: print(f"Not found: {path}") - return flask.make_response( - f"Invalid path: {path}", - HTTPStatus.NOT_FOUND, - ) + return _text_response(HTTPStatus.NOT_FOUND, f"Invalid path: {path}") except Exception as error: if dev_mode: @@ -234,10 +239,7 @@ def _handle_local_file_request(request: LocalFileRequest) -> Response: # swallow it - user likely surfed away from # review screen before an image had finished # downloading - return flask.make_response( - str(error), - HTTPStatus.INTERNAL_SERVER_ERROR, - ) + return _text_response(HTTPStatus.INTERNAL_SERVER_ERROR, str(error)) def _builtin_data(path: str) -> bytes: @@ -270,10 +272,7 @@ def _handle_builtin_file_request(request: BundledFileRequest) -> Response: except FileNotFoundError: if dev_mode: print(f"404: {data_path}") - resp = flask.make_response( - f"Invalid path: {path}", - HTTPStatus.NOT_FOUND, - ) + resp = _text_response(HTTPStatus.NOT_FOUND, f"Invalid path: {path}") # we're including the path verbatim in our response, so we need to either use # plain text, or escape HTML characters to avoid reflecting untrusted input resp.headers["Content-type"] = "text/plain" @@ -288,10 +287,7 @@ def _handle_builtin_file_request(request: BundledFileRequest) -> Response: # swallow it - user likely surfed away from # review screen before an image had finished # downloading - return flask.make_response( - str(error), - HTTPStatus.INTERNAL_SERVER_ERROR, - ) + return _text_response(HTTPStatus.INTERNAL_SERVER_ERROR, str(error)) @app.route("/", methods=["GET", "POST"]) @@ -310,10 +306,7 @@ def handle_request(pathin: str) -> Response: if isinstance(req, NotFound): print(req.message) - return flask.make_response( - f"Invalid path: {pathin}", - HTTPStatus.NOT_FOUND, - ) + return _text_response(HTTPStatus.NOT_FOUND, f"Invalid path: {pathin}") elif callable(req): return _handle_dynamic_request(req) elif isinstance(req, BundledFileRequest): @@ -321,10 +314,7 @@ def handle_request(pathin: str) -> Response: elif isinstance(req, LocalFileRequest): return _handle_local_file_request(req) else: - return flask.make_response( - f"unexpected request: {pathin}", - HTTPStatus.FORBIDDEN, - ) + return _text_response(HTTPStatus.FORBIDDEN, f"unexpected request: {pathin}") def is_sveltekit_page(path: str) -> bool: @@ -665,12 +655,10 @@ def _extract_collection_post_request(path: str) -> DynamicRequest | NotFound: response = flask.make_response(data) response.headers["Content-Type"] = "application/binary" else: - response = flask.make_response("", HTTPStatus.NO_CONTENT) + response = _text_response(HTTPStatus.NO_CONTENT, "") except Exception as exc: print(traceback.format_exc()) - response = flask.make_response( - str(exc), HTTPStatus.INTERNAL_SERVER_ERROR - ) + response = _text_response(HTTPStatus.INTERNAL_SERVER_ERROR, str(exc)) return response return wrapped @@ -718,7 +706,7 @@ def _handle_dynamic_request(req: DynamicRequest) -> Response: try: return req() except Exception as e: - return flask.make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR) + return _text_response(HTTPStatus.INTERNAL_SERVER_ERROR, str(e)) def legacy_page_data() -> Response: @@ -726,7 +714,7 @@ def legacy_page_data() -> Response: if html := aqt.mw.mediaServer.get_page_html(id): return Response(html, mimetype="text/html") else: - return flask.make_response("page not found", HTTPStatus.NOT_FOUND) + return _text_response(HTTPStatus.NOT_FOUND, "page not found") def _extract_page_context() -> PageContext: