* Portfolio 카테고리에 포스팅한 글들은 경력기술서나 포트폴리오에 첨부한 프로젝트 관련되어 작성한 내용임을 알립니다.
* 내용에 대한 댓글은 언제나 환영입니다.
❗ 사내 보안 취약점 대응 포스팅은 #2 사내 보안 취약점 대응1 - 패킷 파라미터 변조 / #3 사내 보안 취약점 대응2 - XSS 및 서버 정보 노출 두개의 포스팅으로 나뉘어져 있습니다.
다음 글 링크 : https://fadet-coding.tistory.com/99
1. 관련 프로젝트 및 업무 개요
* 사내 진행했던 프로젝트로 github repository나 실제 프로덕트 코드는 비공개이며 포스팅의 모든 사항은 공개 가능한 범위에서만 작성합니다.
프로젝트 및 업무 명(공개 가능)
사내 보안 취약점 분석 및 수정
개발 환경(공개 가능)
Vue.js 2.6.x
intelliJ
JAVA 1.8 / Spring Boot 2.7.x / myBatis 2.3
Fiddler Classic / BurpSuite
Tomcat 9.0.x / Jenkins 2.3x / Azure Kubernetes Service
2. 문제 분석 및 최초 Revision
* 포스팅에 사용된 코드 및 이미지는 실제 프로덕트에 사용되지 않은 전부 예시임
문제사항
보안 보고서를 기반으로 현재 내부 시스템 취약점 분석 및 수정 요구
단, 현실적으로 모든 OWASP 권고 사항에 대한 이행은 불가능하므로 critical한 취약점에 대한 수정 진행
(이번 포스트에선 1번, 다음 포스트에서 2,3번 항목에 대해 기술)
1 특정 수정 및 삭제 비즈니스 로직에서 HTTP 요청 패킷의 쿼리 파라미터 변조시 타 유저 데이터 변경 가능 (이번 포스트)
2 클라이언트 input에 XSS 공격 시도시 악의적인 script 작동
3 특정 url 요청시 WAS 서버 정보 노출
근거
# (1-3 공통) BurpSuite 분석
# (1-2 공통) fiddler debugging을 사용한 모의 해킹 시도
# (3) 특정 URL 접속시 서버 정보 노출(staging, production 공통)
분석
1 HTTP 요청 패킷의 서버 수신 성공으로 인한 검증 로직 부재 의심 (이번 포스트)
> WAS에 파라미터 검증 로직 존재 x
2 네트워크 통신 패킷 진입 전 클라이언트 submit부터 <script ... 코드 필터링 제외
> Vue.js 기반 클라이언트의 v-html 태그의 XSS 공격 취약점 발견
3 특정 URL의 에러 발생 시점이 dispatcher servlet 진입 이전임을 확인
> staging, production 내부 server.xml의 VALVE 설정 변경을 통해 서버 정보 노출 레벨 조정 가능
논의 및 학습
#1 특정 수정 및 삭제 비즈니스 로직에서 HTTP 요청 패킷의 쿼리 파라미터 조작 변조시 타 유저 데이터 변경 가능
위 취약점은 Insecure Direct Object Reference (IDOR) 취약점 유형으로 분류되며 서버 측 검증이 필요하다는 판단으로 아래 순서로 논의 후 적용 방식 결정
1-1 세션 request 기반 인증 정보를 통해 검증 가능한가?
> 기존에 서버 세션을 사용하지 않으므로 검증만을 위해 세션을 활성화시키는 것은 부적절하다는 결론
1-2 개별 비즈니스 로직마다 선행 검증을 추가하면 매번 요구사항 발생마다 비용이 들지 않나?
위 논의 사항은 매우 적절하여 다음과 같은 대체 방식을 고려
1. Spring Security의 @PreAuthorize나 AuthorizationManager 사용
2. 별도의 공통 인가용 AuthorizationService 적용
3. 리포지토리 레벨에서 소유권 보장
4. Custom AOP advice 적용 후 annotation을 통한 검증
하지만 위 모든 대체 방식은 다음의 이유로 논의 과정에서 제외
1. Spring Security 사용
> 현재 시스템의 Interceptor 기반 인증/인가에서 Spring Security 도입은 학습/적용 비용 문제로 인해 현실적으로 불가능
PostResponseDto findByIdAndUserId(Long postId, Long userId);
2. 위와 같은 리포지토리 레벨에서 소유권 보장
> 위 방식은 mapper 작성시 누락 가능성이 높아 개발자에게 책임을 전가하는 것이므로 논의에서 제외
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckOwnership {
String resourceIdParam();
}
3. 위와 같은 Custom AOP advice 생성 후 annotation을 통한 검증
> 1 AOP 기반으로 검증을 모듈화시 개발자들의 추가 AOP 학습 비용이 너무 큼 2 AOP의 경우 런타임 프록시를 통해 동작하므로 런타임 성능 오버헤드 발생 가능성 3 유지보수를 위한 디버깅이 복잡해지고 @Transactional 같은 다른 AOP 기능과의 충돌 우려 같은 이유들로 반려
@Service
public class AuthorizationService {
public void checkUserId(Long resourceUserId, Long authenticatedUserId) {
if (!resourceUserId.equals(authenticatedUserId)) {
throw new AccessDeniedException("권한 없음");
}
}
}
4. 위와 같은 별도의 공통 인가용 AuthorizationService 적용
> 현재 적용시 오히려 service를 두번 호출하게 되어 코드 가독성과 SRP 위반으로 서비스가 비대해져 유지보수 비용이 더 커지므로 반려. 하지만 1 서비스간 책임 분리를 통한 SRP 준수 2 비즈니스 로직별 공통 검증이 많아질 경우 매우 효율적 이라는 근거로 인해 추후 논의 예정
1-3 그렇다면 검증을 진행할 component는?
이론상 서버 측 객체별 접근 제어는 주요 layer 마다 적용해야 하는 것이 맞으나 실무적으로 보안 책임을 집중시키는 것이 맞다는 판단 후
검증을 적용할 component 후보는 controller, service, mapper(repository)
이 중 비즈니스 로직별 유저 소유 확인 가능한 component는 service와 mapper
/* 예시 코드로 글의 길이를 위해 format을 일부러 줄임 */
<update id="updatePost" parameterType="map">
<selectKey keyProperty="ValidUserId" resultType="long" order="BEFORE">
SELECT user_id FROM posts
WHERE post_id = #{postId}
</selectKey>
UPDATE posts SET title = '${title}', content = '${content}'
WHERE post_id = ${postId} AND user_id = ${ValidUserId}
</update>
이 경우
1 mapper에서 selectKey를 사용하여 record filtering도 가능은 하지만 이럴 경우 쓸모 없는 DBMS 사용이 발생
2 하나의 update 트랜잭션이라 하더라도 selectKey를 사용하면 원자적인 작업 단위가 아니므로 트랜잭션 격리 수준이나 옵티마이저의 실행 방식에 따라서 안전하지 않을 수 있기 때문에 의존적인 selectKey 사용은 지양
> 최종 논의 결과 : Service layer에 비즈니스 로직별 추가 검증 로직 추가
3. 결과
개선사항
#1 DB 기반 검증 추가
// 예시: 게시글 수정 시 선행 검증 추가
PostResponseDto post = postMapper.findById(postId);
if (!post.getUserId().equals(authenticatedUserId)) {
throw new AccessDeniedException("권한 없음");
}
* AccessDeniedException를 서비스 레이어에서 throw하여 ControllerAdvice가 잡아 공통 403 error로 클라이언트에 응답
테스트
# 사내 fiddler debugging, BurpSuite 테스트시
환경 | 개선 전 파라미터 변조 공격 | 개선 후 파라미터 변조 공격 | 비고 |
Staging | passed | denied | 없음 |
Production | passed | denied | 없음 |
# 이후 협력사 테스트 결과도 완료
4. 추후 이슈 및 대응
이슈 발생 사항
#1 JWT 보안 취약점 발견
기존 인증/인가를 위해 서버 session 대신 JWT token + sessionStorage 를 사용하였기에 HttpOnly인 쿠키가 아닌sessionStorage로 브라우저 저장시 JWT token은 XSS 공격을 통해 탈취 가능성이 높은데
문제는 애플리케이션에 사용되는 JWT token decode시 PAYLOAD에 user primary key가 그대로 노출됨
if (!post.getUserId().equals(authenticatedUserId))
따라서 위와 같은 검증의 경우 HTTP 요청 파라미터에 authenticatedUserId을 탈취한 JWT PAYLOAD에 포함된 pk 값으로 변경할 경우 보안이 뚫리는 문제가 발생
@ 쉽게 단순히 PAYLOAD에 pk값을 지우면 되지 않나?
> 현재 인증/인가 로직에서 JWT token claim을 사용하는 로직들을 전부 같이 수정해야하는데 이는 현실적으로 불가능
결론
현재 악의적인 사용자가 JWT PAYLOAD로 정보를 알게 될 가능성이 높은 상황이지만 결국 서버가 발급하는 JWT 토큰의 서명과 인코딩 방식을 알지 못하면 변조 자체가 거의 불가능
> 서버 검증 로직을 기존 DB 레코드 기반에 더해 JWT Claim 기반 검증을 추가
단, JWT PAYLOAD 노출에 대한 보완이 없다면 이후 진행되는 모든 유지보수 과정에 반드시 이 점이 고려되어야 함
추가 고려 사항
#1 이전 논의처럼 검증 공통 로직이 많아질 경우 공통 인가용 AuthorizationService 도입 고려
* 다음 포스트에서 보안 취약점 대응 - 나머지 2, 3번 항목으로 이어짐
'Portfolio' 카테고리의 다른 글
#4 실무 IntelliJ+Git 사용 (0) | 2025.09.07 |
---|---|
#3 사내 보안 취약점 대응 2 (0) | 2025.09.06 |
#1 CompletableFuture를 이용한 비동기화 (0) | 2025.09.04 |