Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ dependencies {
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
implementation 'org.bouncycastle:bcpkix-jdk15on:1.70'

// Image Processing
implementation 'net.coobird:thumbnailator:0.4.20'
implementation 'org.sejda.imageio:webp-imageio:0.1.6'
Comment on lines +96 to +99
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

echo "=== Checking Thumbnailator Latest Version ==="
curl -s "https://search.maven.org/solrsearch/select?q=g:net.coobird+AND+a:thumbnailator&rows=1&wt=json" | jq -r '.response.docs[0] | "\(.latestVersion) - Released: \(.timestamp)"'

echo -e "\n=== Checking webp-imageio Latest Version ==="
curl -s "https://search.maven.org/solrsearch/select?q=g:org.sejda.imageio+AND+a:webp-imageio&rows=1&wt=json" | jq -r '.response.docs[0] | "\(.latestVersion) - Released: \(.timestamp)"'

echo -e "\n=== Checking Thumbnailator Version History ==="
curl -s "https://search.maven.org/solrsearch/select?q=g:net.coobird+AND+a:thumbnailator&rows=20&wt=json" | jq -r '.response.docs[] | "v\(.v) - \(.timestamp)"' | head -10

echo -e "\n=== Checking webp-imageio Version History ==="
curl -s "https://search.maven.org/solrsearch/select?q=g:org.sejda.imageio+AND+a:webp-imageio&rows=20&wt=json" | jq -r '.response.docs[] | "v\(.v) - \(.timestamp)"' | head -10

Repository: UPbrella/UPbrella_back

Length of output: 361


🌐 Web query:

Thumbnailator security vulnerabilities CVE 2024 2025

💡 Result:

Short answer: I found CVEs affecting "thumbnail" software in 2024–2025 (notably Matrix Media Repo and several WordPress thumbnail plugins), but I found no CVE entries for the Java Thumbnailator library (coobird/thumbnailator).

Notable CVEs (2024–2025)

  • CVE-2024-56515 — Matrix Media Repo (thumbnailers could invoke ImageMagick/ffmpeg decoders; fixed in MMR v1.3.8). [1]
  • CVE-2025-53282 — Stored XSS in the "Thumbnail Editor" WordPress plugin. [2]
  • CVE-2025-12154 — Arbitrary file upload in the "Auto Thumbnailer" WordPress plugin (authenticated). [3]

No CVE for Java Thumbnailator

  • The official Thumbnailator GitHub repository (coobird/thumbnailator) and its security/advisory records show no published CVE/security advisory for the library as of now. [4]

Sources

  1. CVE-2024-56515 details (Recorded Future / NVD references). [1]
  2. CVE-2025-53282 advisory summary (Wiz / Patchstack reporting). [2]
  3. CVE-2025-12154 (NVD / CVE Details summary). [3]
  4. Thumbnailator (coobird/thumbnailator) GitHub repository — no CVE listed in repo advisories. [4]

If you want, I can:

  • search NVD/GHSA/NPM/Maven Central again and return exact NVD/GHSA links and publish dates, or
  • monitor for new CVEs for Thumbnailator and notify you. Which would you prefer?

🌐 Web query:

webp-imageio org.sejda.imageio security advisories vulnerabilities

💡 Result:

Summary — webp-imageio (org.sejda.imageio)

  • I found no public CVE or vendor security advisory that names org.sejda.imageio/webp-imageio specifically. [1][2]
  • The project’s GitHub issues show a few crash/segfault and libwebp-related issues (including an open issue mentioning updating libwebp for CVE-2023-1999), so some security-relevant dependency concerns have been raised upstream. Check the repo issues for details. [3]
  • Maven Central metadata lists webp-imageio 0.1.6 (May 2020) and shows dependency-vulnerability metadata (e.g., CVE-2020-15250 appears in the package page), indicating you should also audit its dependencies. [2]

Relevant sources

  1. Maven / package page for org.sejda.imageio:webp-imageio (artifact, version + vulnerability metadata). [2]
  2. N/A for org.sejda.imageio-specific CVE — but ImageIO-related CVEs (Java ImageIO component) that are relevant to image-processing libraries: CVE-2020-14562, CVE-2022-21277, CVE-2021-35586 (examples from NVD/Rapid7/Wiz). These are general ImageIO CVEs to be aware of. [4][5][6]
  3. sejda-pdf/webp-imageio GitHub issues (open issues include crashes and “Update libwebp CVE-2023-1999”). Monitor this repo for fixes. [3]

If you want, I can:

  • Run a live dependency scan (OSS advisories/CVE lookup) against a specific webp-imageio version (e.g., 0.1.6) and list any dependency CVEs.
  • Watch the GitHub repo and notify you of new security issues.

Address unmaintained webp-imageio dependency.

Thumbnailator 0.4.20 is the latest version (released June 2023) with no known security advisories. However, webp-imageio 0.1.6 has not been updated since May 2020 and appears unmaintained. Its underlying libwebp dependency is outdated and has known vulnerabilities (e.g., CVE-2023-1999). Consider evaluating alternative WebP processing libraries or submitting upstream patches to the sejda project to address the stale dependencies.

🤖 Prompt for AI Agents
In build.gradle around lines 96–99 the project pulls in the unmaintained
org.sejda.imageio:webp-imageio:0.1.6 which relies on an outdated libwebp with
known vulnerabilities; remove this dependency and replace it with a maintained
WebP solution (either a maintained ImageIO WebP plugin or a native library
wrapper) by: (1) researching and selecting a maintained WebP processing library
or using a native toolchain (e.g., a maintained imageio-webp fork or a JNI
binding to libwebp), (2) updating build.gradle to remove the sejda:webp-imageio
entry and add the chosen dependency, (3) adapting image loading/encoding call
sites to the new API and adding tests, and if you must keep sejda temporarily
open an issue/PR against their repo and add a dependency exclusion/pinning and a
comment documenting the risk until a fix is merged.

}

// Kotlin 컴파일 옵션 설정
Expand Down
24 changes: 24 additions & 0 deletions src/main/kotlin/upbrella/be/config/ImageProcessingConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package upbrella.be.config

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.ConstructorBinding
import org.springframework.context.annotation.Configuration

@Configuration
@ConfigurationProperties(prefix = "image.processing")
@ConstructorBinding
data class ImageProcessingConfig(
val sizes: ImageSizes = ImageSizes(),
val quality: ImageQuality = ImageQuality()
)
Comment on lines +7 to +13
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Incorrect usage of @ConstructorBinding with @configuration.

In Spring Boot 2.7+, @ConstructorBinding should not be combined with @Configuration. This annotation combination can prevent proper configuration binding.

🔎 Recommended fix

Option 1 (Preferred): Remove @configuration and enable via @EnableConfigurationProperties

Remove @Configuration from the data class and enable it elsewhere (e.g., in a main configuration class or application class):

-@Configuration
 @ConfigurationProperties(prefix = "image.processing")
 @ConstructorBinding
 data class ImageProcessingConfig(
     val sizes: ImageSizes = ImageSizes(),
     val quality: ImageQuality = ImageQuality()
 )

Then add @EnableConfigurationProperties(ImageProcessingConfig::class) to your main application class or another configuration class.

Option 2: Remove @ConstructorBinding and keep @configuration

 @Configuration
 @ConfigurationProperties(prefix = "image.processing")
-@ConstructorBinding
 data class ImageProcessingConfig(
     val sizes: ImageSizes = ImageSizes(),
     val quality: ImageQuality = ImageQuality()
 )

This allows Spring to use setter-based binding instead of constructor binding.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Configuration
@ConfigurationProperties(prefix = "image.processing")
@ConstructorBinding
data class ImageProcessingConfig(
val sizes: ImageSizes = ImageSizes(),
val quality: ImageQuality = ImageQuality()
)
@Configuration
@ConfigurationProperties(prefix = "image.processing")
data class ImageProcessingConfig(
val sizes: ImageSizes = ImageSizes(),
val quality: ImageQuality = ImageQuality()
)
🤖 Prompt for AI Agents
In src/main/kotlin/upbrella/be/config/ImageProcessingConfig.kt around lines 7 to
13, the data class is annotated with @ConstructorBinding and @Configuration
which is incorrect for Spring Boot 2.7+; remove the @Configuration annotation
from this data class and register the properties class via
@EnableConfigurationProperties(ImageProcessingConfig::class) on your main
application or a configuration class (alternatively, if you prefer to keep
@Configuration, remove @ConstructorBinding to use setter-based binding),
ensuring ImageProcessingConfig remains a pure @ConfigurationProperties data
class.


data class ImageSizes(
val thumbnail: Int = 300,
val medium: Int = 800,
val large: Int = 1200
)

data class ImageQuality(
val webp: Float = 0.85f,
val jpeg: Float = 0.90f
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package upbrella.be.store.dto.response

data class ImageUrlsResponse(
val id: Long?,
val webp: ImageSizeUrls?,
val jpeg: ImageSizeUrls
)

data class ImageSizeUrls(
val thumb: String,
val medium: String,
val large: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@ import upbrella.be.store.entity.StoreImage

data class SingleImageUrlResponse(
val id: Long?,
val imageUrl: String?
val imageUrl: String?, // 기존 호환성 유지
val imageUrls: ImageUrlsResponse
) {
companion object {
fun createImageUrlResponse(imageUrl: StoreImage) = SingleImageUrlResponse(
id = imageUrl.id,
imageUrl = imageUrl.imageUrl
)
fun createImageUrlResponse(storeImage: StoreImage): SingleImageUrlResponse {
val imageUrlsResponse = storeImage.getImageUrlsResponse()
return SingleImageUrlResponse(
id = storeImage.id,
imageUrl = storeImage.imageUrl, // 기존 호환성
imageUrls = imageUrlsResponse
)
}
}
}
43 changes: 43 additions & 0 deletions src/main/kotlin/upbrella/be/store/entity/StoreImage.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package upbrella.be.store.entity

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import upbrella.be.store.dto.response.ImageSizeUrls
import upbrella.be.store.dto.response.ImageUrlsResponse
import javax.persistence.*

@Entity
Expand All @@ -8,17 +12,56 @@ class StoreImage(
@JoinColumn(name = "store_detail_id")
val storeDetail: StoreDetail? = null,
var imageUrl: String? = null,

@Column(name = "image_urls", columnDefinition = "TEXT")
var imageUrls: String? = null,

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
) {
companion object {
private val objectMapper: ObjectMapper = jacksonObjectMapper()

@JvmStatic
fun createStoreImage(storeDetail: StoreDetail, imageUrl: String): StoreImage {
return StoreImage(
storeDetail = storeDetail,
imageUrl = imageUrl,
)
}

@JvmStatic
fun createStoreImageWithUrls(
storeDetail: StoreDetail,
imageUrlsResponse: ImageUrlsResponse
): StoreImage {
return StoreImage(
storeDetail = storeDetail,
imageUrl = imageUrlsResponse.jpeg.medium, // 기존 호환성
imageUrls = objectMapper.writeValueAsString(imageUrlsResponse),
)
}
}

/**
* 다중 URL 구조 반환 (JSON 파싱 또는 기존 URL fallback)
*/
fun getImageUrlsResponse(): ImageUrlsResponse {
return if (imageUrls != null) {
// 새 포맷: JSON 파싱
objectMapper.readValue(imageUrls, ImageUrlsResponse::class.java)
} else {
// 기존 이미지: 단일 URL을 모든 크기에 사용
ImageUrlsResponse(
id = id,
webp = null,
jpeg = ImageSizeUrls(
thumb = imageUrl ?: "",
medium = imageUrl ?: "",
large = imageUrl ?: ""
)
)
}
}
Comment on lines +50 to 66
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add error handling for JSON parsing.

The getImageUrlsResponse() method performs JSON parsing without error handling. If the stored JSON is malformed or incompatible, this will throw unchecked exceptions during entity access, potentially breaking application flows.

🔎 Recommended fix
 fun getImageUrlsResponse(): ImageUrlsResponse {
     return if (imageUrls != null) {
-        // 새 포맷: JSON 파싱
-        objectMapper.readValue(imageUrls, ImageUrlsResponse::class.java)
+        try {
+            // 새 포맷: JSON 파싱
+            objectMapper.readValue(imageUrls, ImageUrlsResponse::class.java)
+        } catch (e: Exception) {
+            // JSON 파싱 실패 시 fallback
+            ImageUrlsResponse(
+                id = id,
+                webp = null,
+                jpeg = ImageSizeUrls(
+                    thumb = imageUrl ?: "",
+                    medium = imageUrl ?: "",
+                    large = imageUrl ?: ""
+                )
+            )
+        }
     } else {
         // 기존 이미지: 단일 URL을 모든 크기에 사용
         ImageUrlsResponse(
             id = id,
             webp = null,
             jpeg = ImageSizeUrls(
                 thumb = imageUrl ?: "",
                 medium = imageUrl ?: "",
                 large = imageUrl ?: ""
             )
         )
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun getImageUrlsResponse(): ImageUrlsResponse {
return if (imageUrls != null) {
// 새 포맷: JSON 파싱
objectMapper.readValue(imageUrls, ImageUrlsResponse::class.java)
} else {
// 기존 이미지: 단일 URL을 모든 크기에 사용
ImageUrlsResponse(
id = id,
webp = null,
jpeg = ImageSizeUrls(
thumb = imageUrl ?: "",
medium = imageUrl ?: "",
large = imageUrl ?: ""
)
)
}
}
fun getImageUrlsResponse(): ImageUrlsResponse {
return if (imageUrls != null) {
try {
// 새 포맷: JSON 파싱
objectMapper.readValue(imageUrls, ImageUrlsResponse::class.java)
} catch (e: Exception) {
// JSON 파싱 실패 시 fallback
ImageUrlsResponse(
id = id,
webp = null,
jpeg = ImageSizeUrls(
thumb = imageUrl ?: "",
medium = imageUrl ?: "",
large = imageUrl ?: ""
)
)
}
} else {
// 기존 이미지: 단일 URL을 모든 크기에 사용
ImageUrlsResponse(
id = id,
webp = null,
jpeg = ImageSizeUrls(
thumb = imageUrl ?: "",
medium = imageUrl ?: "",
large = imageUrl ?: ""
)
)
}
}
🤖 Prompt for AI Agents
In src/main/kotlin/upbrella/be/store/entity/StoreImage.kt around lines 50 to 66,
the JSON parsing of imageUrls is performed without error handling; wrap the
objectMapper.readValue call in a try-catch (catch
JsonProcessingException/IOException or Exception), log the parse error with
context (id and the raw imageUrls) using the class logger, and on error return
the same legacy fallback ImageUrlsResponse (using imageUrl for all sizes) so
malformed JSON does not throw and entity access continues.

}
164 changes: 164 additions & 0 deletions src/main/kotlin/upbrella/be/store/service/ImageProcessingService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package upbrella.be.store.service

import net.coobird.thumbnailator.Thumbnails
import org.springframework.stereotype.Service
import org.springframework.web.multipart.MultipartFile
import upbrella.be.config.ImageProcessingConfig
import java.awt.image.BufferedImage
import java.io.ByteArrayOutputStream
import javax.imageio.ImageIO

@Service
class ImageProcessingService(
private val config: ImageProcessingConfig
) {

enum class ImageSize(val maxSize: Int) {
THUMBNAIL(300),
MEDIUM(800),
LARGE(1200);

companion object {
fun from(configValue: Int): ImageSize {
return when (configValue) {
300 -> THUMBNAIL
800 -> MEDIUM
1200 -> LARGE
else -> throw IllegalArgumentException("Unknown image size: $configValue")
}
}
}
}

/**
* 이미지를 3가지 크기 x 2가지 포맷(WebP, JPEG)으로 처리
*/
fun processImage(file: MultipartFile): ProcessedImageSet {
val originalImage = ImageIO.read(file.inputStream)
?: throw IllegalArgumentException("Invalid image file")

// 3가지 크기로 리사이징
val thumbnail = processSize(originalImage, config.sizes.thumbnail)
val medium = processSize(originalImage, config.sizes.medium)
val large = processSize(originalImage, config.sizes.large)

return ProcessedImageSet(thumbnail, medium, large)
}

/**
* 특정 크기로 리사이징하고 WebP와 JPEG 포맷으로 변환
*/
private fun processSize(originalImage: BufferedImage, targetSize: Int): ImageVariantData {
// 리사이징
val resized = resizeImage(originalImage, targetSize)

// WebP 변환
val webp = convertToWebP(resized, config.quality.webp)

// JPEG 변환
val jpeg = convertToJpeg(resized, config.quality.jpeg)

return ImageVariantData(webp, jpeg)
}

/**
* 이미지를 지정된 크기로 리사이징 (비율 유지)
*/
private fun resizeImage(image: BufferedImage, maxSize: Int): BufferedImage {
return Thumbnails.of(image)
.size(maxSize, maxSize)
.asBufferedImage()
}

/**
* BufferedImage를 WebP 포맷으로 변환
*/
private fun convertToWebP(image: BufferedImage, quality: Float): ByteArray {
val outputStream = ByteArrayOutputStream()

// WebP ImageWriter 설정
val writer = ImageIO.getImageWritersByFormatName("webp").next()
?: throw IllegalStateException("WebP ImageWriter not found. Make sure webp-imageio is on the classpath")

val writeParam = writer.defaultWriteParam
if (writeParam.canWriteCompressed()) {
writeParam.compressionMode = javax.imageio.ImageWriteParam.MODE_EXPLICIT
writeParam.compressionQuality = quality
}

val output = ImageIO.createImageOutputStream(outputStream)
writer.output = output
writer.write(null, javax.imageio.IIOImage(image, null, null), writeParam)
writer.dispose()
output.close()

return outputStream.toByteArray()
}

/**
* BufferedImage를 JPEG 포맷으로 변환
*/
private fun convertToJpeg(image: BufferedImage, quality: Float): ByteArray {
val outputStream = ByteArrayOutputStream()

// RGB 이미지로 변환 (JPEG는 투명도 미지원)
val rgbImage = if (image.type == BufferedImage.TYPE_INT_ARGB ||
image.type == BufferedImage.TYPE_4BYTE_ABGR) {
val newImage = BufferedImage(image.width, image.height, BufferedImage.TYPE_INT_RGB)
val g = newImage.createGraphics()
g.drawImage(image, 0, 0, null)
g.dispose()
newImage
} else {
image
}

val writer = ImageIO.getImageWritersByFormatName("jpeg").next()
val writeParam = writer.defaultWriteParam
writeParam.compressionMode = javax.imageio.ImageWriteParam.MODE_EXPLICIT
writeParam.compressionQuality = quality

val output = ImageIO.createImageOutputStream(outputStream)
writer.output = output
writer.write(null, javax.imageio.IIOImage(rgbImage, null, null), writeParam)
writer.dispose()
output.close()

return outputStream.toByteArray()
}
}

/**
* 3가지 크기의 이미지 데이터 세트
*/
data class ProcessedImageSet(
val thumbnail: ImageVariantData,
val medium: ImageVariantData,
val large: ImageVariantData
)

/**
* 각 크기별 WebP와 JPEG 데이터
*/
data class ImageVariantData(
val webp: ByteArray,
val jpeg: ByteArray
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as ImageVariantData

if (!webp.contentEquals(other.webp)) return false
if (!jpeg.contentEquals(other.jpeg)) return false

return true
}

override fun hashCode(): Int {
var result = webp.contentHashCode()
result = 31 * result + jpeg.contentHashCode()
return result
}
}
Loading