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
16 changes: 16 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,22 @@ dependencies {
//Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'

//QueryDSL
implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
annotationProcessor 'com.querydsl:querydsl-apt:5.1.0:jakarta'
annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
annotationProcessor 'jakarta.persistence:jakarta.persistence-api'

Comment on lines +51 to +56
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# QueryDSL 최신 버전 확인
echo "=== QueryDSL 최신 버전 확인 ==="
curl -s "https://search.maven.org/solrsearch/select?q=g:com.querydsl+AND+a:querydsl-jpa&rows=5&wt=json" | jq -r '.response.docs[] | "\(.v)"' | head -5

echo ""
echo "=== Maven Central에서 5.1.0 검색 ==="
curl -s "https://search.maven.org/solrsearch/select?q=g:com.querydsl+AND+a:querydsl-jpa+AND+v:5.1.0&wt=json" | jq -r '.response.docs[] | "Version: \(.v), Timestamp: \(.timestamp)"'

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 175


🏁 Script executed:

#!/bin/bash
# Maven Central에서 QueryDSL 정보 더 자세히 확인
echo "=== Maven Central API로 최신 버전 확인 ==="
curl -s "https://search.maven.org/solrsearch/select?q=g:com.querydsl+AND+a:querydsl-jpa&rows=20&wt=json&sort=timestamp+desc" | jq '.response.docs[0:5] | .[] | {version: .v, timestamp: .timestamp}'

echo ""
echo "=== QueryDSL 보안 정보 확인 ==="
# NVD에서 QueryDSL 취약점 확인
curl -s "https://services.nvd.nist.gov/rest/json/cves/2.0?keywordSearch=querydsl" | jq '.vulnerabilities[]? | {id: .cve.id, summary: .cve.descriptions[0].value}' | head -20

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 497


🏁 Script executed:

#!/bin/bash
# QueryDSL 5.1.x 및 6.x 버전 확인
echo "=== QueryDSL 5.1.x 패치 버전 확인 ==="
curl -s "https://search.maven.org/solrsearch/select?q=g:com.querydsl+AND+a:querydsl-jpa+AND+v:5.1.*&rows=10&wt=json" | jq '.response.docs[] | {version: .v, timestamp: .timestamp}' | sort

echo ""
echo "=== QueryDSL 최신 안정 버전 (6.x) 확인 ==="
curl -s "https://search.maven.org/solrsearch/select?q=g:com.querydsl+AND+a:querydsl-jpa&rows=30&wt=json" | jq '.response.docs | sort_by(.timestamp) | reverse | .[0:3] | .[] | {version: .v, timestamp: .timestamp}'

echo ""
echo "=== CVE-2024-49203 상세 정보 ==="
curl -s "https://services.nvd.nist.gov/rest/json/cves/2.0?keywordSearch=CVE-2024-49203" | jq '.vulnerabilities[0] | {id: .cve.id, published: .cve.published, modified: .cve.lastModified, severity: .containers.cna.metrics[0].cvssV3_1.baseScore}'

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 406


QueryDSL 5.1.0에 SQL/HQL 인젝션 취약점(CVE-2024-49203) 발견

현재 사용 중인 QueryDSL 5.1.0에 CVE-2024-49203 취약점이 있습니다(2024년 11월 공개). 이는 JPAQuery의 orderBy에서 SQL/HQL 인젝션을 허용하는 취약점입니다. 신뢰할 수 없는 입력값을 orderBy에 직접 사용하지 않는다면 즉시적인 위험은 낮지만, 5.1.0은 패치 버전이 없으므로 최신 버전으로 업그레이드하는 것을 권장합니다.

🤖 Prompt for AI Agents
In build.gradle around lines 51 to 56, QueryDSL 5.1.0 is used which is
vulnerable to CVE-2024-49203; replace the QueryDSL coordinates (implementation
and both annotationProcessor entries) with a patched QueryDSL release (upgrade
5.1.0 → the latest patched version that addresses CVE-2024-49203), update the
version consistently for all three entries, refresh Gradle dependencies (e.g.,
./gradlew --refresh-dependencies), rebuild and run tests, and audit any use of
JPAQuery.orderBy to ensure untrusted input is not directly passed (sanitize or
whitelist ordering fields if needed).

}

def generated = "build/generated/sources/annotationProcessor/java/main"

sourceSets {
main {
java {
srcDirs += [generated]
}
}
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package team.wego.wegobackend.common.config;

import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class QuerydslConfig {

@PersistenceContext
private EntityManager entityManager;

@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package team.wego.wegobackend.common.support;

import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import team.wego.wegobackend.common.security.Role;
import team.wego.wegobackend.user.domain.Follow;
import team.wego.wegobackend.user.domain.User;

/**
* local 테스트 데이터 초기화를 위한 설정
* */
@Configuration
@Profile("local")
public class LocalDataInitializer implements ApplicationRunner {

private final EntityManager em;

public LocalDataInitializer(EntityManager em) {
this.em = em;
}

@Override
@Transactional
public void run(ApplicationArguments args) throws Exception {
// test user ~100
for(long i = 0; i <= 100; i++) {
em.persist(User.builder()
.email("user" + i + "@test.com")
.password("1q2w3e4r!")
.nickName("user" + i)
.role(Role.ROLE_USER)
.build()
);
}

// user1 → user2~50
for (long i = 2; i <= 50; i++) {
em.persist(new Follow(
em.getReference(User.class, 1L),
em.getReference(User.class, i)
));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
Expand All @@ -30,7 +31,7 @@ public class GroupImageController implements GroupImageControllerDocs {

private final GroupImageService groupImageService;

@PostMapping("/upload")
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<ApiResponse<PreUploadGroupImageResponse>> uploadImages(
@AuthenticationPrincipal CustomUserDetails userDetails,
@RequestParam("images") List<MultipartFile> images
Expand All @@ -43,7 +44,7 @@ public ResponseEntity<ApiResponse<PreUploadGroupImageResponse>> uploadImages(
.body(ApiResponse.success(HttpStatus.CREATED.value(), response));
}

@PatchMapping("/{groupId}")
@PatchMapping(value = "/{groupId}")
public ResponseEntity<ApiResponse<List<GroupImageItemResponse>>> updateGroupImages(
@AuthenticationPrincipal CustomUserDetails userDetails,
@PathVariable Long groupId,
Expand All @@ -58,7 +59,7 @@ public ResponseEntity<ApiResponse<List<GroupImageItemResponse>>> updateGroupImag
}


@DeleteMapping("/{groupId}")
@DeleteMapping(value = "/{groupId}")
public ResponseEntity<ApiResponse<Void>> deleteGroupImages(
@AuthenticationPrincipal CustomUserDetails userDetails,
@PathVariable Long groupId
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package team.wego.wegobackend.user.application;

import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import team.wego.wegobackend.user.application.dto.response.FollowListResponse;
import team.wego.wegobackend.user.application.dto.response.FollowResponse;
import team.wego.wegobackend.user.domain.Follow;
import team.wego.wegobackend.user.domain.User;
import team.wego.wegobackend.user.exception.ExistFollowException;
Expand Down Expand Up @@ -68,4 +71,17 @@ public void unFollow(String unFollowNickname, Long followerId) {
follower.decreaseFolloweeCount();
follow.decreaseFollowerCount();
}

public FollowListResponse followList(Long userId, Long cursor, Integer size) {

if(!userRepository.existsById(userId)) {
throw new UserNotFoundException();
}

List<FollowResponse> list = followRepository.findFollowingList(userId, cursor, size);

Long nextCursor = list.isEmpty() ? null : list.getLast().getFollowId();

return new FollowListResponse(list, nextCursor);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package team.wego.wegobackend.user.application.dto.response;

import java.util.List;

public record FollowListResponse(List<FollowResponse> items, Long nextCursor) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package team.wego.wegobackend.user.application.dto.response;

import com.querydsl.core.annotations.QueryProjection;
import lombok.Getter;

@Getter
public class FollowResponse {

private final Long followId; //cursor
private final Long userId;
private final String profileImage;
private final String nickname;
private final String profileMessage;

@QueryProjection
public FollowResponse(
Long followId,
Long userId,
String profileImage,
String nickname,
String profileMessage
) {
this.followId = followId;
this.userId = userId;
this.profileImage = profileImage;
this.nickname = nickname;
this.profileMessage = profileMessage;
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package team.wego.wegobackend.user.presentation;

import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import java.time.LocalDateTime;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
Expand All @@ -25,6 +28,7 @@
import team.wego.wegobackend.user.application.UserService;
import team.wego.wegobackend.user.application.dto.request.ProfileUpdateRequest;
import team.wego.wegobackend.user.application.dto.response.AvailabilityResponse;
import team.wego.wegobackend.user.application.dto.response.FollowListResponse;
import team.wego.wegobackend.user.application.dto.response.UserInfoResponse;

@Slf4j
Expand Down Expand Up @@ -53,7 +57,7 @@ public ResponseEntity<ApiResponse<UserInfoResponse>> profile(@PathVariable Long
/**
* 프로필 이미지 변경
* */
@PatchMapping("/profile-image")
@PatchMapping(value = "/profile-image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<ApiResponse<UserInfoResponse>> profileImage(
@AuthenticationPrincipal CustomUserDetails userDetails,
@RequestPart("file") MultipartFile file
Expand Down Expand Up @@ -131,6 +135,23 @@ public ResponseEntity<ApiResponse<String>> unFollow(
.body(ApiResponse.success(200, "팔로우 취소 성공"));
}

/**
* 팔로우 리스트 조회
* */
@GetMapping("/{userId}/follow")
public ResponseEntity<ApiResponse<FollowListResponse>> followList(
@PathVariable Long userId, //다른 유저 조회를 위한 파라메터
@RequestParam(required = false) Long cursor,
@RequestParam(defaultValue = "20") @Min(1) @Max(100) Integer size
) {
Comment on lines +141 to +146
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:

cat -n src/main/java/team/wego/wegobackend/user/presentation/UserController.java | head -20

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 1201


🏁 Script executed:

rg -n "@Validated|@RestController|@RequestMapping" src/main/java/team/wego/wegobackend/user/presentation/UserController.java | head -20

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 125


🏁 Script executed:

sed -n '30,45p' src/main/java/team/wego/wegobackend/user/presentation/UserController.java

Repository: WeGo-Together/WeGo_BackEnd

Length of output: 587


클래스에 @Validated 어노테이션 추가 필요

@RequestParam@Min(1)@Max(100) 검증이 작동하려면 컨트롤러 클래스에 @Validated 어노테이션이 필수입니다. 현재 클래스 선언부에 해당 어노테이션이 없습니다.

클래스 선언부에 @Validated 추가:

+import org.springframework.validation.annotation.Validated;
+
 @Slf4j
 @RestController
 @RequiredArgsConstructor
+@Validated
 @RequestMapping("/api/v1/users")
 public class UserController implements UserControllerDocs {

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/team/wego/wegobackend/user/presentation/UserController.java
around lines 141 to 146, the @Min/@Max constraints on the @RequestParam 'size'
won't be enforced because the controller class is missing the Spring Validation
trigger; add the @Validated annotation to the controller class declaration
(importing org.springframework.validation.annotation.Validated if needed) so
that parameter-level JSF validation annotations are activated by Spring MVC.


FollowListResponse response = followService.followList(userId, cursor, size);

return ResponseEntity
.status(HttpStatus.OK)
.body(ApiResponse.success(200, response));
}

/**
* 이메일 중복검사
* */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package team.wego.wegobackend.user.presentation;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PathVariable;
Expand All @@ -14,6 +17,7 @@
import team.wego.wegobackend.common.security.CustomUserDetails;
import team.wego.wegobackend.user.application.dto.request.ProfileUpdateRequest;
import team.wego.wegobackend.user.application.dto.response.AvailabilityResponse;
import team.wego.wegobackend.user.application.dto.response.FollowListResponse;
import team.wego.wegobackend.user.application.dto.response.UserInfoResponse;

@Tag(name = "유저 API", description = "유저와 관련된 API 리스트 \uD83D\uDC08")
Expand Down Expand Up @@ -52,6 +56,13 @@ ResponseEntity<ApiResponse<String>> unFollow(
@Valid @RequestParam("unFollowNickname") String unFollowNickname
);

@Operation(summary = "팔로우 리스트 조회 API", description = "userId에 해당하는 유저의 팔로우 리스트를 조회합니다.")
ResponseEntity<ApiResponse<FollowListResponse>> followList(
@Parameter(description = "조회할 유저 ID") @PathVariable Long userId,
@Parameter(description = "페이지네이션 커서 (첫 페이지는 null)") @RequestParam(required = false) Long cursor,
@Parameter(description = "조회 개수 (1-100)") @RequestParam(defaultValue = "20") @Min(1) @Max(100) Integer size
);

@Operation(summary = "이메일 중복 검사 API", description = "이메일 중복검사에 대한 응답은 bool값입니다.")
ResponseEntity<ApiResponse<AvailabilityResponse>> checkEmailAvailability(
@RequestParam("email") String email
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import org.springframework.data.jpa.repository.JpaRepository;
import team.wego.wegobackend.user.domain.Follow;

public interface FollowRepository extends JpaRepository<Follow, Long> {
public interface FollowRepository extends JpaRepository<Follow, Long>, FollowRepositoryCustom {

Optional<Follow> findByFollowerIdAndFolloweeId(Long followerId, Long followingId);
boolean existsByFollowerIdAndFolloweeId(Long followerId, Long followingId);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package team.wego.wegobackend.user.repository;

import java.util.List;
import team.wego.wegobackend.user.application.dto.response.FollowResponse;

public interface FollowRepositoryCustom {

List<FollowResponse> findFollowingList(
Long followerId,
Long cursorFollowId,
int size
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package team.wego.wegobackend.user.repository;

import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.List;
import lombok.RequiredArgsConstructor;
import team.wego.wegobackend.user.application.dto.response.FollowResponse;
import team.wego.wegobackend.user.application.dto.response.QFollowResponse;
import team.wego.wegobackend.user.domain.QFollow;
import team.wego.wegobackend.user.domain.QUser;

@RequiredArgsConstructor
public class FollowRepositoryImpl implements FollowRepositoryCustom{

private final JPAQueryFactory jpaQueryFactory;

@Override
public List<FollowResponse> findFollowingList(
Long followerId,
Long cursorFollowId,
int size
) {

QFollow follow = QFollow.follow;
QUser user = QUser.user;

return jpaQueryFactory
.select(new QFollowResponse(
follow.id,
user.id,
user.profileImage,
user.nickName,
user.profileMessage
))
.from(follow)
.join(follow.followee, user)
.where(
follow.follower.id.eq(followerId),
ItCursor(cursorFollowId)
)
.orderBy(follow.id.desc()) //최신 팔로우 순
.limit(size)
.fetch();
}

private BooleanExpression ItCursor(Long cursorFollowId) {
return cursorFollowId == null
? null
: QFollow.follow.id.lt(cursorFollowId);
}
}
16 changes: 15 additions & 1 deletion src/test/http/user/user-api.http
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ POST http://localhost:8080/api/v1/auth/login
Content-Type: application/json

{
"email": "test@example.com",
"email": "test@test.com",
"password": "Test1234!@#"
}

Expand Down Expand Up @@ -76,3 +76,17 @@ GET http://localhost:8080/api/v1/users/email/availability

### 닉네임 중복검사
GET http://localhost:8080/api/v1/users/nickname/availability?nickname=test

### 팔로우 리스트 조회 (초기값)
GET http://localhost:8080/api/v1/users/1/follow?size=10

> {%
client.global.set("nextCursor", response.body.data.nextCursor);
%}

### 팔로우 리스트 조회 (cursor)
GET http://localhost:8080/api/v1/users/1/follow?cursor={{nextCursor}}&size=10

> {%
client.global.set("nextCursor", response.body.data.nextCursor);
%}