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
@@ -1,6 +1,7 @@
package com.example.RealMatch.brand.domain.repository;

import java.util.List;
import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
Expand All @@ -14,4 +15,6 @@ public interface BrandAvailableSponsorRepository extends JpaRepository<BrandAvai

@Query("SELECT s FROM BrandAvailableSponsor s LEFT JOIN FETCH s.images WHERE s.brand.id = :brandId")
List<BrandAvailableSponsor> findByBrandIdWithImages(@Param("brandId") Long brandId);

Optional<BrandAvailableSponsor> findByBrandIdAndId(Long brandId, Long id);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package com.example.RealMatch.business.application.service;

import java.util.Optional;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.example.RealMatch.brand.domain.entity.BrandAvailableSponsor;
import com.example.RealMatch.brand.domain.repository.BrandAvailableSponsorRepository;
import com.example.RealMatch.business.domain.entity.CampaignProposal;
import com.example.RealMatch.business.domain.repository.CampaignProposalRepository;
import com.example.RealMatch.business.exception.BusinessErrorCode;
Expand All @@ -17,14 +21,22 @@
public class CampaignProposalQueryService {

private final CampaignProposalRepository campaignProposalRepository;
private final BrandAvailableSponsorRepository brandAvailableSponsorRepository;

public CampaignProposalDetailResponse getProposalDetail(
Long userId,
Long proposalId
) {
CampaignProposal proposal = campaignProposalRepository.findByIdWithTags(proposalId)
.orElseThrow(() -> new CustomException(BusinessErrorCode.CAMPAIGN_PROPOSAL_NOT_FOUND));
Comment on lines 30 to 31
Copy link
Contributor

Choose a reason for hiding this comment

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

security-high high

The getProposalDetail method takes a userId and a proposalId but fails to verify if the user is authorized to access the proposal. An attacker could potentially view any campaign proposal by providing its ID. The code even contains a TODO comment acknowledging the missing authorization check. Implement an authorization check to ensure the userId matches either the senderUserId or receiverUserId of the proposal (or has appropriate administrative privileges).

Suggested change
CampaignProposal proposal = campaignProposalRepository.findByIdWithTags(proposalId)
.orElseThrow(() -> new CustomException(BusinessErrorCode.CAMPAIGN_PROPOSAL_NOT_FOUND));
CampaignProposal proposal = campaignProposalRepository.findByIdWithTags(proposalId)
.orElseThrow(() -> new CustomException(BusinessErrorCode.CAMPAIGN_PROPOSAL_NOT_FOUND));
if (!proposal.getSenderUserId().equals(userId) && !proposal.getReceiverUserId().equals(userId)) {
throw new CustomException(BusinessErrorCode.CAMPAIGN_PROPOSAL_FORBIDDEN);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

추후 권한 확인 로직 추가 예정 (데모데이 이후)


// TODO: 데모데이 이후에 해당 제품이 없으면 에러 던지는 방향으로 수정 필요!! + 조회 권한 로직 추가 필요(본인만 조회 가능)
String productName = Optional.ofNullable(proposal.getProductId())
.flatMap(productId -> brandAvailableSponsorRepository
.findByBrandIdAndId(proposal.getBrand().getId(), productId))
.map(BrandAvailableSponsor::getName)
.orElse(null);

return CampaignProposalDetailResponse.from(proposal);
return CampaignProposalDetailResponse.from(proposal, productName);
Comment on lines +33 to +40
Copy link
Contributor

Choose a reason for hiding this comment

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

security-high high

The getProposalDetail method currently lacks an authorization check, leading to an Insecure Direct Object Reference (IDOR) vulnerability. The userId parameter is passed but unused, allowing unauthorized access to campaign proposal details. It is critical to verify that the userId matches either the senderUserId or receiverUserId of the proposal before returning the response. Additionally, please add a comment to clarify why Optional.ofNullable is used for the productId field in the CampaignProposal entity, given that productId is marked as nullable=false.

        if (!proposal.getSenderUserId().equals(userId) && !proposal.getReceiverUserId().equals(userId)) {
            throw new CustomException(BusinessErrorCode.CAMPAIGN_PROPOSAL_USER_MISMATCH);
        }

        String productName = Optional.ofNullable(proposal.getProductId())
                .flatMap(productId -> brandAvailableSponsorRepository
                        .findByBrandIdAndId(proposal.getBrand().getId(), productId))
                .map(BrandAvailableSponsor::getName)
                .orElse(null);

        return CampaignProposalDetailResponse.from(proposal, productName);

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public class CampaignProposalDetailResponse {

private Long rewardAmount;
private Long productId;
private String productName;

private LocalDate startDate;
private LocalDate endDate;
Expand All @@ -36,7 +37,7 @@ public class CampaignProposalDetailResponse {

private CampaignContentTagResponse contentTags;

public static CampaignProposalDetailResponse from(CampaignProposal proposal) {
public static CampaignProposalDetailResponse from(CampaignProposal proposal, String productName) {
Campaign campaign = proposal.getCampaign();

return CampaignProposalDetailResponse.builder()
Expand All @@ -49,6 +50,7 @@ public static CampaignProposalDetailResponse from(CampaignProposal proposal) {
.description(proposal.getCampaignDescription())
.rewardAmount(Long.valueOf(proposal.getRewardAmount()))
.productId(proposal.getProductId())
.productName(productName)
.startDate(proposal.getStartDate())
.endDate(proposal.getEndDate())
.status(proposal.getStatus().name())
Expand Down