From b2f736b4e9e160b43f074a0777e6a44a3aff6926 Mon Sep 17 00:00:00 2001 From: dkgkdfg65 <219107372+dkgkdfg65@users.noreply.github.com> Date: Sun, 17 May 2026 00:11:15 +0800 Subject: [PATCH] fix(security): Mandate content-type on POST calls (CVE-2025-53095) Hand-ported subset of upstream LizardByte/Sunshine 738ac93a0ec1cd10412d1f339968775f53bfefe0: adds check_content_type() helper and calls it at the start of POST handlers for /api/apps, /api/apps/close, /api/apps/ (DELETE), /api/clients/unpair, /api/clients/unpair-all, /api/config, /api/covers/upload, /api/password, /api/pin, /api/restart, and /api/reset-display-device-persistence. AlkaidLab does not have upstreams bad_request() helper, so the bad-request response is inlined inside check_content_type() as a local lambda; the behavior matches upstream (HTTP 400 + JSON body with status_code/status/error). AlkaidLab-specific POST endpoints (AI/LLM proxy at /api/ai/config and /api/ai/chat/completions, QR pairing at /api/qr-pair{,/cancel}, client rename at /api/clients/rename, /api/apps/test-menu-cmd, /api/logout) are intentionally NOT covered by this PR; reviewer may extend the same check to those if desired. HTML-side Content-Type-on-fetch changes from upstream are skipped: AlkaidLab UI is heavily restructured for Chinese localization + AI UI and using a different request framework. (cherry picked from commit 738ac93a0ec1cd10412d1f339968775f53bfefe0) Signed-off-by: dkgkdfg65 <219107372+dkgkdfg65@users.noreply.github.com> --- src/confighttp.cpp | 49 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/confighttp.cpp b/src/confighttp.cpp index e5ad3b708c4..f787359f0c8 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -133,6 +133,49 @@ namespace confighttp { response->write(output_tree.dump(), headers); } + /** + * @brief Validate the request content type and send bad request when mismatch. + * @param response The HTTP response object. + * @param request The HTTP request object. + * @param contentType The expected content type. + * @return true if the request's Content-Type matches the expected type. + * + * Backport of upstream LizardByte/Sunshine 738ac93a0ec1 (CVE-2025-53095). + * AlkaidLab does not have upstream's bad_request() helper, so the bad-request + * response is inlined here. + */ + bool check_content_type(resp_https_t response, req_https_t request, const std::string &contentType) { + auto send_bad_request = [response](const std::string &error_message) { + nlohmann::json tree; + tree["status_code"] = 400; + tree["status"] = false; + tree["error"] = error_message; + SimpleWeb::CaseInsensitiveMultimap headers; + headers.emplace("Content-Type", "application/json"); + response->write(SimpleWeb::StatusCode::client_error_bad_request, tree.dump(), headers); + }; + auto requestContentType = request->header.find("content-type"); + if (requestContentType == request->header.end()) { + send_bad_request("Content type not provided"); + return false; + } + // Extract the media type part before any parameters (e.g., charset) + std::string actualContentType = requestContentType->second; + size_t semicolonPos = actualContentType.find(';'); + if (semicolonPos != std::string::npos) { + actualContentType = actualContentType.substr(0, semicolonPos); + } + boost::algorithm::trim(actualContentType); + boost::algorithm::to_lower(actualContentType); + std::string expectedContentType(contentType); + boost::algorithm::to_lower(expectedContentType); + if (actualContentType != expectedContentType) { + send_bad_request("Content type mismatch"); + return false; + } + return true; + } + void send_unauthorized(resp_https_t response, req_https_t request) { auto address = net::addr_to_normalized_string(request->remote_endpoint().address()); @@ -728,6 +771,7 @@ namespace confighttp { void saveApp(resp_https_t response, req_https_t request) { + if (!check_content_type(response, request, "application/json")) return; if (!authenticate(response, request)) return; print_req(request); @@ -891,6 +935,7 @@ namespace confighttp { void uploadCover(resp_https_t response, req_https_t request) { + if (!check_content_type(response, request, "application/json")) return; if (!authenticate(response, request)) return; std::stringstream ss; @@ -1157,6 +1202,7 @@ namespace confighttp { void saveConfig(resp_https_t response, req_https_t request) { + if (!check_content_type(response, request, "application/json")) return; if (!authenticate(response, request)) return; print_req(request); @@ -1255,6 +1301,7 @@ namespace confighttp { void savePassword(resp_https_t response, req_https_t request) { + if (!check_content_type(response, request, "application/json")) return; if (!config::sunshine.username.empty() && !authenticate(response, request)) return; print_req(request); @@ -1325,6 +1372,7 @@ namespace confighttp { void savePin(resp_https_t response, req_https_t request) { + if (!check_content_type(response, request, "application/json")) return; if (!authenticate(response, request)) return; print_req(request); @@ -1560,6 +1608,7 @@ namespace confighttp { void unpair(resp_https_t response, req_https_t request) { + if (!check_content_type(response, request, "application/json")) return; if (!authenticate(response, request)) return; print_req(request);