Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -73,18 +73,18 @@ public DataResponse<List<ProductOptionResponse>> getProductOptions(
public DataResponse<ProductSearchPageResDto> getProducts(
@RequestParam(required = false) String query,
@RequestParam(required = false) Long categoryId,
@RequestParam(required = false) Long brandId,
@RequestParam(required = false) List<Long> brandIds,
@RequestParam(required = false) String priceRange,
@RequestParam(required = false) String clothingSize,
@RequestParam(required = false) List<String> clothingSizes,
@RequestParam(required = false, defaultValue = "POPULAR") ProductSortType sortType,
@PageableDefault(size = 20) Pageable pageable,
@AuthenticationPrincipal Long userId
) {
ProductSearchCondition condition = ProductSearchCondition.builder()
.categoryId(categoryId)
.brandId(brandId)
.brandIds(brandIds != null && !brandIds.isEmpty() ? brandIds : null)
.priceRange(priceRange)
.size(clothingSize)
.sizes(clothingSizes != null && !clothingSizes.isEmpty() ? clothingSizes : null)
.build();

ProductSearchPageResDto res = productService.getProducts(condition, sortType, pageable, query, userId);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.ongil.backend.domain.product.dto.request;

import java.util.List;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;
Expand All @@ -12,14 +14,23 @@ public class ProductSearchCondition {
@Schema(description = "카테고리 ID", example = "1")
private Long categoryId;

@Schema(description = "브랜드 ID", example = "5")
private Long brandId;
@Schema(description = "브랜드 ID 목록 (다중 선택)", example = "[1, 2, 3]")
private List<Long> brandIds;

@Schema(description = "가격 범위 (형식: minPrice-maxPrice)", example = "50000-100000")
private String priceRange;

@Schema(description = "사이즈 (예: XS, S, M, L, XL)", example = "M")
private String size;
@Schema(description = "사이즈 목록 (다중 선택, 예: XS, S, M, L, XL)", example = "[M, L]")
private List<String> sizes;
Comment on lines +23 to +24
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

@Schema example 값이 유효하지 않은 JSON 형식

example = "[M, L]" 는 JSON 배열 형식이 아닙니다. Swagger UI에서 잘못된 예시로 표시됩니다.

📝 제안 수정
-@Schema(description = "사이즈 목록 (다중 선택, 예: XS, S, M, L, XL)", example = "[M, L]")
+@Schema(description = "사이즈 목록 (다중 선택, 예: XS, S, M, L, XL)", example = "[\"M\", \"L\"]")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/ongil/backend/domain/product/dto/request/ProductSearchCondition.java`
around lines 23 - 24, The `@Schema` example on the sizes field in
ProductSearchCondition uses "[M, L]" which is not valid JSON; update the `@Schema`
annotation for the sizes field to provide a valid JSON array string (e.g. a
quoted JSON array with each element quoted) so Swagger displays it correctly and
ensure any inner quotes are properly escaped in the Java string literal.


// 사이즈 목록을 REGEXP 패턴으로 변환 (예: [M, L] → "(^|,)(M|L)(,|$)")
public String buildSizesPattern() {
if (sizes == null || sizes.isEmpty()) {
return null;
}
String sizeGroup = String.join("|", sizes);
return "(^|,)(" + sizeGroup + ")(,|$)";
}
Comment on lines +27 to +33
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

buildSizesPattern() — 사용자 입력값이 정규식 패턴에 그대로 삽입되는 Regex Injection 취약점

sizes는 HTTP 쿼리 파라미터에서 직접 오는 사용자 입력값입니다. 현재 이 값들을 아무런 이스케이프 없이 정규식 문자열에 이어붙이고 있어서, 악의적인 사용자가 sizes=["M)|((.*)"]와 같은 값을 넘기면 패턴이 (^|,)(M)|((.*))(,|$)로 변형되어 사이즈 필터를 우회하고 모든 상품을 반환할 수 있습니다.

수정 방법은 두 가지입니다:

  1. 허용된 사이즈 값(enum 또는 정규식)으로 화이트리스트 검증 (권장)
  2. 각 사이즈 값에서 정규식 메타문자 제거
🛡️ 제안 수정 (화이트리스트 방식)
 public String buildSizesPattern() {
     if (sizes == null || sizes.isEmpty()) {
         return null;
     }
-    String sizeGroup = String.join("|", sizes);
+    // 영숫자와 '+'만 허용하여 정규식 메타문자 삽입 차단
+    String sizeGroup = sizes.stream()
+        .map(s -> s.replaceAll("[^a-zA-Z0-9+]", ""))
+        .filter(s -> !s.isEmpty())
+        .collect(java.util.stream.Collectors.joining("|"));
+    if (sizeGroup.isEmpty()) {
+        return null;
+    }
     return "(^|,)(" + sizeGroup + ")(,|$)";
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/ongil/backend/domain/product/dto/request/ProductSearchCondition.java`
around lines 27 - 33, The buildSizesPattern() method concatenates raw
user-provided sizes into a regex and must be fixed to prevent regex injection:
in ProductSearchCondition, validate each entry of the sizes list against a
whitelist of allowed size tokens (e.g., your Size enum or a precompiled Set of
valid values) and discard/throw on invalid inputs, or alternatively escape each
value before joining (use Java's Pattern.quote for each size) inside
buildSizesPattern(); then build the pattern from the validated/escaped values so
the returned string is safe.


// 가격 범위 파싱
public Integer[] parsePriceRange() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,23 @@ public interface ProductRepository extends JpaRepository<Product, Long> {
SELECT p FROM Product p
WHERE (:targetIds IS NULL OR p.id IN :targetIds)
AND (:categoryId IS NULL OR p.category.id = :categoryId)
AND (:brandId IS NULL OR p.brand.id = :brandId)
AND (:minPrice IS NULL OR p.price >= :minPrice)
AND (:maxPrice IS NULL OR p.price <= :maxPrice)
AND (:size IS NULL OR p.sizes LIKE CONCAT('%', :size, '%'))
AND (:brandIds IS NULL OR p.brand.id IN :brandIds)
AND (:minPrice IS NULL OR
(p.discountPrice IS NOT NULL AND p.discountPrice > 0 AND p.discountPrice >= :minPrice)
OR ((p.discountPrice IS NULL OR p.discountPrice = 0) AND p.price >= :minPrice))
AND (:maxPrice IS NULL OR
(p.discountPrice IS NOT NULL AND p.discountPrice > 0 AND p.discountPrice <= :maxPrice)
OR ((p.discountPrice IS NULL OR p.discountPrice = 0) AND p.price <= :maxPrice))
AND (:sizesPattern IS NULL OR FUNCTION('REGEXP_LIKE', p.sizes, :sizesPattern) = true)
AND p.onSale = true
""")
Page<Product> findAllByCondition(
@Param("targetIds") List<Long> targetIds,
@Param("categoryId") Long categoryId,
@Param("brandId") Long brandId,
@Param("brandIds") List<Long> brandIds,
@Param("minPrice") Integer minPrice,
@Param("maxPrice") Integer maxPrice,
@Param("size") String size,
@Param("sizesPattern") String sizesPattern,
Pageable pageable
);

Expand All @@ -66,19 +70,23 @@ Page<Product> findAllByCondition(
SELECT p FROM Product p
WHERE (:targetIds IS NULL OR p.id IN :targetIds)
AND p.category.parentCategory.id = :parentCategoryId
AND (:brandId IS NULL OR p.brand.id = :brandId)
AND (:minPrice IS NULL OR p.price >= :minPrice)
AND (:maxPrice IS NULL OR p.price <= :maxPrice)
AND (:size IS NULL OR p.sizes LIKE CONCAT('%', :size, '%'))
AND (:brandIds IS NULL OR p.brand.id IN :brandIds)
AND (:minPrice IS NULL OR
(p.discountPrice IS NOT NULL AND p.discountPrice > 0 AND p.discountPrice >= :minPrice)
OR ((p.discountPrice IS NULL OR p.discountPrice = 0) AND p.price >= :minPrice))
AND (:maxPrice IS NULL OR
(p.discountPrice IS NOT NULL AND p.discountPrice > 0 AND p.discountPrice <= :maxPrice)
OR ((p.discountPrice IS NULL OR p.discountPrice = 0) AND p.price <= :maxPrice))
AND (:sizesPattern IS NULL OR FUNCTION('REGEXP_LIKE', p.sizes, :sizesPattern) = true)
AND p.onSale = true
""")
Page<Product> findAllByParentCategoryCondition(
@Param("targetIds") List<Long> targetIds,
@Param("parentCategoryId") Long parentCategoryId,
@Param("brandId") Long brandId,
@Param("brandIds") List<Long> brandIds,
@Param("minPrice") Integer minPrice,
@Param("maxPrice") Integer maxPrice,
@Param("size") String size,
@Param("sizesPattern") String sizesPattern,
Pageable pageable
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,21 +160,21 @@ public ProductSearchPageResDto getProducts(
products = productRepository.findAllByParentCategoryCondition(
targetIds,
condition.getCategoryId(),
condition.getBrandId(),
condition.getBrandIds(),
minPrice,
maxPrice,
condition.getSize(),
condition.buildSizesPattern(),
pageableWithSort
);
} else {
// 하위 카테고리 → 해당 카테고리 상품만 조회
products = productRepository.findAllByCondition(
targetIds,
condition.getCategoryId(),
condition.getBrandId(),
condition.getBrandIds(),
minPrice,
maxPrice,
condition.getSize(),
condition.buildSizesPattern(),
pageableWithSort
);
}
Expand All @@ -183,10 +183,10 @@ public ProductSearchPageResDto getProducts(
products = productRepository.findAllByCondition(
targetIds,
null,
condition.getBrandId(),
condition.getBrandIds(),
minPrice,
maxPrice,
condition.getSize(),
condition.buildSizesPattern(),
pageableWithSort
);
}
Expand Down