From f7ec75b070bc93c0b0b2f40f86047db087d55319 Mon Sep 17 00:00:00 2001 From: ktppp Date: Fri, 21 Aug 2026 15:05:48 +0900 Subject: [PATCH] =?UTF-8?q?mass=20asignment=E5=AF=BE=E7=AD=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controllers/api_videos.cpp | 23 +++++++++++--- docs/openapi.yml | 2 ++ test/controllers.cpp | 63 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 4 deletions(-) diff --git a/controllers/api_videos.cpp b/controllers/api_videos.cpp index 0144917..2678590 100644 --- a/controllers/api_videos.cpp +++ b/controllers/api_videos.cpp @@ -358,12 +358,22 @@ drogon::Task videos::getVideo([[maybe_unused]] HttpRequ drogon::Task videos::patchVideo([[maybe_unused]] HttpRequestPtr req, std::string id) { auto jsonPtr = req->getJsonObject(); - if (!jsonPtr) { + if (!jsonPtr || !jsonPtr->isObject() || jsonPtr->empty()) { auto resp = drogon::HttpResponse::newHttpResponse(); resp->setStatusCode(drogon::HttpStatusCode::k400BadRequest); - resp->setBody("Invalid JSON format"); + resp->setBody("Request body must be a non-empty JSON object"); co_return resp; } + + for (const auto& key : jsonPtr->getMemberNames()) { + if ((key != "title" && key != "description") || !(*jsonPtr)[key].isString()) { + auto resp = drogon::HttpResponse::newHttpResponse(); + resp->setStatusCode(drogon::HttpStatusCode::k400BadRequest); + resp->setBody("Only string fields 'title' and 'description' can be updated"); + co_return resp; + } + } + drogon::orm::CoroMapper mapper(drogon::app().getDbClient()); try { auto video = co_await mapper.findByPrimaryKey(id); @@ -373,7 +383,12 @@ drogon::Task videos::patchVideo([[maybe_unused]] HttpRe resp->setBody("You are not the owner of this video"); co_return resp; } - video.updateByJson(*jsonPtr); + if (jsonPtr->isMember("title")) { + video.setTitle((*jsonPtr)["title"].asString()); + } + if (jsonPtr->isMember("description")) { + video.setDescription((*jsonPtr)["description"].asString()); + } co_await mapper.update(video); auto resp = drogon::HttpResponse::newHttpJsonResponse(video.toJson()); @@ -929,4 +944,4 @@ drogon::Task videos::getThumbnails(HttpRequestPtr req, co_return resp; } co_return co_await getVideoThumbnails(req, id, filename); -} \ No newline at end of file +} diff --git a/docs/openapi.yml b/docs/openapi.yml index b965c5f..b1aae16 100644 --- a/docs/openapi.yml +++ b/docs/openapi.yml @@ -197,6 +197,8 @@ paths: application/json: schema: type: object + minProperties: 1 + additionalProperties: false properties: title: type: string diff --git a/test/controllers.cpp b/test/controllers.cpp index 945a8d0..d093699 100644 --- a/test/controllers.cpp +++ b/test/controllers.cpp @@ -207,6 +207,69 @@ DROGON_TEST(EditVideoTest) CHECK(deleteVideo(videoId) == true); } +DROGON_TEST(EditVideoMassAssignmentTest) +{ + std::optional videoIdOpt = postVideo("変更されないタイトル", "変更されない説明"); + REQUIRE(videoIdOpt.has_value()); + const std::string videoId = videoIdOpt.value(); + + auto beforeResp = sendSyncRequest(drogon::Get, "/api/videos/" + videoId); + REQUIRE(beforeResp != nullptr); + REQUIRE(beforeResp->getStatusCode() == drogon::k200OK); + auto beforeJsonPtr = beforeResp->getJsonObject(); + REQUIRE(beforeJsonPtr != nullptr); + const Json::Value beforeJson = *beforeJsonPtr; + + const std::vector> protectedFields = { + {"video_id", "attacker-video"}, + {"user_id", "otheruser"}, + {"thumbnail_url", "https://example.com/example.png"}, + {"video_url", "https://example.com/watch/attacker-video"}, + {"created_at", "2008-04-23 00:31:07"}, + {"view_count", 999999999}, + {"duration", 8}, + {"like_count", 999999999}, + {"status", 2}, + {"is_external", 1}, + {"type", "youtube"}, + }; + + for (const auto& [field, value] : protectedFields) { + Json::Value body; + body["title"] = "攻撃者が変更したタイトル"; + body[field] = value; + auto resp = sendSyncRequest(drogon::Patch, "/api/videos/" + videoId, body); + REQUIRE(resp != nullptr); + CHECK(resp->getStatusCode() == drogon::k400BadRequest); + } + + const std::vector invalidBodies = { + Json::Value(Json::objectValue), + Json::Value(Json::arrayValue), + Json::Value(42), + }; + for (const auto& body : invalidBodies) { + auto resp = sendSyncRequest(drogon::Patch, "/api/videos/" + videoId, body); + REQUIRE(resp != nullptr); + CHECK(resp->getStatusCode() == drogon::k400BadRequest); + } + + Json::Value nonStringTitle; + nonStringTitle["title"] = 42; + auto invalidTypeResp = sendSyncRequest(drogon::Patch, "/api/videos/" + videoId, nonStringTitle); + REQUIRE(invalidTypeResp != nullptr); + CHECK(invalidTypeResp->getStatusCode() == drogon::k400BadRequest); + + auto afterResp = sendSyncRequest(drogon::Get, "/api/videos/" + videoId); + REQUIRE(afterResp != nullptr); + REQUIRE(afterResp->getStatusCode() == drogon::k200OK); + auto afterJson = afterResp->getJsonObject(); + REQUIRE(afterJson != nullptr); + CHECK(*afterJson == beforeJson); + + CHECK(deleteVideo(videoId) == true); +} + DROGON_TEST(SearchTest) { // 動画を投稿