From 51f7b5c7d3e1319fb3428f7e58d90e943d50d553 Mon Sep 17 00:00:00 2001 From: joungmin Date: Mon, 15 Jun 2026 15:31:56 +0900 Subject: [PATCH] =?UTF-8?q?feat(restaurant):=20#332=20PUT=20body=20?= =?UTF-8?q?=ED=99=94=EC=9D=B4=ED=8A=B8=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=AA=85=EC=8B=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ALLOWED_UPDATE_FIELDS set으로 PUT /api/restaurants/{id} body를 SQL updateFields 컬럼 가드와 1:1로 매핑. 허용 외 키는 silent drop + DEBUG 로그. 기존 SQL 로 이미 임의 컬럼 갱신이 차단되어 있으나, Controller에 명시 화이트리스트가 없어 의도 모호. 본 변경으로 두 레이어 모두 화이트리스트 확보. sanitized가 비면 200 no-op로 응답 (사용자 경험 우선). DDG/isNameSimilar/DTO는 별도 후속 (예: #346) 분리. 설계서: docs/design/332-restaurant-update-whitelist/README.md Refs: #332 --- .../controller/RestaurantController.java | 49 ++++++++++++++----- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/backend-java/src/main/java/com/tasteby/controller/RestaurantController.java b/backend-java/src/main/java/com/tasteby/controller/RestaurantController.java index 7640f25..9f3fcc4 100644 --- a/backend-java/src/main/java/com/tasteby/controller/RestaurantController.java +++ b/backend-java/src/main/java/com/tasteby/controller/RestaurantController.java @@ -90,15 +90,34 @@ public class RestaurantController { return r; } + // #332 — Restaurant 업데이트 화이트리스트 (SQL updateFields의 컬럼 가드와 1:1). + // 허용되지 않은 키는 무시(silent drop). DTO 도입은 후속 작업. + private static final java.util.Set ALLOWED_UPDATE_FIELDS = java.util.Set.of( + "name", "address", "region", "cuisine_type", "price_range", + "phone", "website", "tabling_url", "catchtable_url", + "latitude", "longitude", "google_place_id", + "business_status", "rating", "rating_count" + ); + @PutMapping("/{id}") public Map update(@PathVariable String id, @RequestBody Map body) { AuthUtil.requireAdmin(); var r = restaurantService.findById(id); if (r == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Restaurant not found"); + // #332 — 입력 body를 허용 키만 통과시킨 가변 Map으로 정규화 + Map sanitized = new java.util.LinkedHashMap<>(); + for (var e : body.entrySet()) { + if (ALLOWED_UPDATE_FIELDS.contains(e.getKey())) { + sanitized.put(e.getKey(), e.getValue()); + } else { + log.debug("Ignoring non-whitelisted update field: {}", e.getKey()); + } + } + // Re-geocode if name or address changed - String newName = (String) body.get("name"); - String newAddress = (String) body.get("address"); + String newName = (String) sanitized.get("name"); + String newAddress = (String) sanitized.get("address"); boolean nameChanged = newName != null && !newName.equals(r.getName()); boolean addressChanged = newAddress != null && !newAddress.equals(r.getAddress()); if (nameChanged || addressChanged) { @@ -106,26 +125,30 @@ public class RestaurantController { String geoAddr = newAddress != null ? newAddress : r.getAddress(); var geo = geocodingService.geocodeRestaurant(geoName, geoAddr); if (geo != null) { - body.put("latitude", geo.get("latitude")); - body.put("longitude", geo.get("longitude")); - body.put("google_place_id", geo.get("google_place_id")); + sanitized.put("latitude", geo.get("latitude")); + sanitized.put("longitude", geo.get("longitude")); + sanitized.put("google_place_id", geo.get("google_place_id")); if (geo.containsKey("formatted_address")) { - body.put("address", geo.get("formatted_address")); + sanitized.put("address", geo.get("formatted_address")); } - if (geo.containsKey("rating")) body.put("rating", geo.get("rating")); - if (geo.containsKey("rating_count")) body.put("rating_count", geo.get("rating_count")); - if (geo.containsKey("phone")) body.put("phone", geo.get("phone")); - if (geo.containsKey("business_status")) body.put("business_status", geo.get("business_status")); + if (geo.containsKey("rating")) sanitized.put("rating", geo.get("rating")); + if (geo.containsKey("rating_count")) sanitized.put("rating_count", geo.get("rating_count")); + if (geo.containsKey("phone")) sanitized.put("phone", geo.get("phone")); + if (geo.containsKey("business_status")) sanitized.put("business_status", geo.get("business_status")); - // formatted_address에서 region 파싱 (예: "대한민국 서울특별시 강남구 ..." → "한국|서울|강남구") String addr = (String) geo.get("formatted_address"); if (addr != null) { - body.put("region", GeocodingService.parseRegionFromAddress(addr)); + sanitized.put("region", GeocodingService.parseRegionFromAddress(addr)); } } } - restaurantService.update(id, body); + if (sanitized.isEmpty()) { + // 허용 키가 하나도 없으면 no-op + return Map.of("ok", true, "restaurant", r); + } + + restaurantService.update(id, sanitized); cache.flush(); var updated = restaurantService.findById(id); return Map.of("ok", true, "restaurant", updated);