1. 개요
이전 글에서는 직접 개발한 Spring Boot 웹 애플리케이션을 대상으로 Docker 기반 보안 진단 환경을 구축했다.
이번에는 구축한 게시판에 Burp Suite를 연결하여 HTTP 요청과 응답을 분석하고, 게시글 수정 기능에서 IDOR(Insecure Direct Object Reference) 취약점이 발생하는지 확인하고자 한다.
IDOR는 서버에서 사용자가 요청한 객체에 대한 접근 권한을 적절하게 검증하지 않을 때 발생할 수 있는 접근통제 취약점이다.
예를 들어 일반 사용자 A가 다른 사용자 B의 게시글 ID를 지정하여 수정 요청을 보냈을 때, 서버가 게시글 소유권을 확인하지 않고 수정한다면 접근통제 취약점에 해당할 수 있다.
이번 분석에서는 정상 사용자와 다른 사용자의 게시글 수정 요청을 비교하고, 실제 데이터베이스의 변경 여부까지 확인했다.
진단 환경
| 구분 |
환경 |
| 분석 대상 | 자체 개발 Spring Boot 게시판 |
| 서버 환경 | Docker, Spring Boot 3.3.11 |
| 데이터베이스 | MySQL 8.0 |
| 분석 도구 | Burp Suite Community Edition |
| 분석 기법 | HTTP 요청 변조 및 소스코드 분석 |
| 진단 항목 | 게시글 수정 기능의 접근통제(IDOR) |
| 대상 URL | http://localhost:8080 |
진단에는 로컬 환경에서 생성한 테스트 계정 두 개를 사용했다.
- jcob: 정상 수정 요청 및 타인 게시글 수정 시도
- jcob2: 타인 게시글 수정 시도의 대상 계정
본 실습은 직접 구축한 로컬 테스트 환경에서 진행했다.
2. Burp Suite 환경 구성 및 HTTP 요청 분석
2.1 Burp Suite 내장 브라우저 연결
Burp Suite를 실행한 후 다음 순서로 진행했다.
- Proxy → Intercept 메뉴 이동
- Intercept off 설정
- Open Browser 클릭
- 내장 브라우저에서 게시판 접속
http://localhost:8080
Intercept off는 요청을 차단하지 않고 서버로 전달하면서 HTTP History에 기록하는 상태다.
이를 통해 정상적인 브라우저 동작을 유지하면서 HTTP 요청과 응답을 분석할 수 있다.


2.2 HTTP 요청 및 응답 확인
게시판에 접속하면 다음과 같은 요청이 발생한다.
GET / HTTP/1.1
Host: localhost:8080
서버에서는 정상적으로 응답했다.
HTTP/1.1 200 OK
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
X-Frame-Options: DENY
주요 응답 헤더는 다음과 같다.
| 헤더 |
의미 |
| X-Content-Type-Options | 브라우저의 MIME 타입 추측 제한 |
| X-XSS-Protection | 구형 브라우저의 XSS 필터 관련 설정 |
| Cache-Control | 응답 데이터 캐싱 정책 |
| X-Frame-Options | iframe 삽입 제한 |
X-XSS-Protection: 0은 구형 XSS 필터를 비활성화하는 설정으로, 해당 값만으로 XSS 취약점이 존재한다고 판단할 수 없다.
또한 로그인 과정에서 JSESSIONID 쿠키가 사용되는 것을 확인했다.
Cookie: JSESSIONID=[REDACTED]
JSESSIONID는 Spring Security의 세션 기반 인증에서 사용되는 세션 식별자다.
이후 Repeater에서 인증된 요청을 재전송할 때 사용되므로, 요청에 포함된 세션 정보가 유효한지 확인해야 한다.

3. 게시글 수정 기능 접근통제(IDOR) 검증
3.1 정상적인 수정 요청 확인
먼저 jcob 계정으로 로그인한 상태에서 본인이 작성한 33번 게시글의 수정 기능을 확인했다.


Burp Suite의 HTTP History에서는 다음 두 요청이 기록됐다.
GET /articles/33/form HTTP/1.1
POST /articles/33/form HTTP/1.1


각 요청의 역할은 다음과 같다.
| HTTP Method |
URL | 기능 |
| GET | /articles/33/form | 게시글 수정 페이지 조회 |
| POST | /articles/33/form | 게시글 수정 내용 저장 |
GET 요청은 수정 화면을 조회하는 용도이고, POST 요청은 입력된 수정 내용을 서버에 전달하는 용도다.
따라서 이번 접근통제 진단에서는 POST 요청을 주요 분석 대상으로 선정했다.
3.2 POST 요청 구조 확인
게시글 수정 요청을 확인한 결과 다음과 같은 구조였다.
POST /articles/33/form HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded
Cookie: JSESSIONID=[REDACTED]
_csrf=[REDACTED]&title=Original-Title&content=Original-Content&hashtag=%23orange
주요 파라미터는 다음과 같다.
| 파라미터 |
설명 |
| articleId | URL 경로에 포함된 수정 대상 게시글 ID |
| _csrf | CSRF 방어를 위한 토큰 |
| title | 게시글 제목 |
| content | 게시글 내용 |
| hashtag | 게시글 해시태그 |
| JSESSIONID | 로그인 세션 식별자 |
특히 URL의 게시글 ID는 클라이언트가 변경할 수 있는 값이므로, 서버 측에서 해당 게시글에 대한 수정 권한을 반드시 확인해야 한다.
4. Burp Repeater를 이용한 정상 수정 요청 검증
4.1 Repeater로 요청 전송
HTTP History에서 다음 요청을 선택했다.
POST /articles/33/form
마우스 오른쪽 버튼을 클릭한 후 Send to Repeater를 선택했다.
Burp Repeater는 HTTP 요청을 직접 수정하고 동일한 서버로 반복 전송할 수 있는 도구다.
Repeater에서는 HTTP 요청 원문을 수정하거나 Inspector에서 개별 파라미터 값을 수정할 수 있다.
이번 실습에서는 Inspector의 Request body parameters를 이용했다.

4.2 요청 파라미터 수정
정상적인 게시글 수정이 수행되는지 확인하기 위해 제목과 내용에 식별 가능한 테스트 문자열을 입력했다.
title=IDOR-TEST-001
content=IDOR-CONTENT-001


이때 로그인 세션과 CSRF 토큰은 그대로 유지했다.
수정한 요청은 기존과 동일하게 다음 엔드포인트로 전송했다.
POST /articles/33/form HTTP/1.1
응답은 다음과 같은 형태로 반환됐다.
HTTP/1.1 302 Found
Location: http://localhost:8080/articles/33

이는 서버가 요청 처리 후 게시글 상세 페이지로 리다이렉트하도록 응답했다는 의미다.
단, 302 Found 응답만으로 게시글 수정 성공 여부를 확정할 수는 없다.
4.3 실제 수정 결과 확인
Burp Suite에서 요청을 전송한 후 브라우저에서 33번 게시글을 조회했다.
GET /articles/33 HTTP/1.1
확인 결과 게시글 제목이 다음과 같이 변경됐다.
IDOR-TEST-001

이를 통해 jcob 계정에서 본인이 작성한 게시글에 대한 수정 요청이 정상적으로 처리되는 것을 확인했다.
5. 타인 게시글 수정 권한 검증
5.1 테스트 시나리오
정상적인 게시글 수정이 가능하다는 것을 확인한 후, 다른 사용자가 작성한 게시글을 수정할 수 있는지 검증했다.
이번 테스트에서는 다음 조건을 사용했다.
| 항목 |
테스트 조건 |
| 로그인 계정 | jcob |
| 정상 게시글 | 33번 (jcob 소유) |
| 수정 대상 게시글 | 75번 (jcob2 소유) |
| 진단 유형 | 타인 게시글 수정 권한 우회 |
| 예상 결과 | 권한 없는 수정 요청 차단 |
즉, jcob으로 로그인한 상태에서 jcob2의 게시글을 수정하려는 시나리오다.
[jcob2 계정의 75번 게시글]

5.2 게시글 ID 및 요청 데이터 변조
정상적으로 처리됐던 수정 요청을 기준으로 URL의 게시글 ID를 변경했다.
변경 전:
POST /articles/33/form HTTP/1.1
변경 후:
POST /articles/75/form HTTP/1.1



수정 여부를 명확하게 구분하기 위해 요청 본문에는 다음 테스트 값을 사용했다.
title=IDOR-TEST-002
content=Unauthorized-Modification-Test
이때 로그인 계정은 jcob으로 유지했다.
실제 진단에서는 다른 사용자로 로그인하는 것이 아니라, 현재 사용자의 인증정보를 유지한 상태에서 접근 대상 객체 ID만 변경하는 것이 중요하다.
5.3 서버 응답 확인
변조한 요청을 Repeater에서 전송했다.
서버는 다음과 같이 응답했다.
HTTP/1.1 302 Found
Location: http://localhost:8080/articles/75

응답은 33번 게시글을 정상 수정했을 때와 유사하게 302 Found로 반환됐다.
하지만 이것만으로 수정이 성공했는지, 권한검사에 의해 차단됐는지 판단할 수 없었다.
따라서 데이터베이스에서 실제 수정 여부를 확인했다.
6. MySQL을 통한 데이터 변경 여부 확인
6.1 MySQL 접속
Docker에서 실행 중인 MySQL 컨테이너에 접속했다.
docker exec -it selfsupport-mysql mysql -u example_user -p board
명령어 구성은 다음과 같다.
| 명령어 |
의미 |
| docker exec | 실행 중인 컨테이너 내부에서 명령어 실행 |
| -it | 대화형 터미널 사용 |
| selfsupport-mysql | MySQL 컨테이너 이름 |
| mysql | MySQL 클라이언트 실행 |
| -u example_user | 데이터베이스 사용자 지정 |
| -p | 비밀번호 입력 |
| board | 조회할 데이터베이스 지정 |
6.2 게시글 조회
다음 SQL을 실행하여 75번 게시글의 정보를 확인했다.
SELECT id, user_id, title, content
FROM article
WHERE id = 75;
SQL 구성은 다음과 같다.
- SELECT: 조회할 컬럼 지정
- FROM article: 게시글 테이블 조회
- WHERE id = 75: 75번 게시글만 조회
확인 결과 다음과 같았다.
id : 75
user_id : jcob2
title : Nullam orci pede, venenatis non, sodales sed, tincidunt eu, felis.
content : Fusce posuere felis sed lacus. Morbi sem mauris, laoreet ut, rhoncus aliquet, pulvinar sed, nisl. Nunc rhoncus dui vel sem.
게시글 내용 역시 기존 데이터가 유지되었다.
즉, Repeater에서 변조한 수정 요청을 전송했지만 데이터베이스의 제목과 내용은 변경되지 않았다.

6.3 진단 결과
| 검증 항목 |
결과 |
| 본인 게시글 수정 | 성공 |
| 타인 게시글 수정 요청 | 전송 가능 |
| 타인 게시글 실제 변경 | 실패 |
| 접근통제 여부 | 소유권 검증 동작 확인 |
| 진단 판정 | 양호 (해당 수정 기능 기준) |
이번 테스트에서는 타인 게시글 수정으로 이어지는 IDOR 취약점이 재현되지 않았다.
7. 소스코드 정적 분석
동적 분석에서 타인 게시글 수정이 차단된 원인을 확인하기 위해 Controller와 Service 코드를 분석했다.
7.1 ArticleController 분석
게시글 수정 요청을 처리하는 Controller는 다음과 같다.
@PostMapping("/{articleId}/form")
public String updateArticle(
@PathVariable Long articleId,
@AuthenticationPrincipal BoardPrincipal boardPrincipal,
ArticleRequest articleRequest
) {
articleService.updateArticle(
articleId,
articleRequest.toDto(boardPrincipal.toDto())
);
return "redirect:/articles/" + articleId;
}
주요 부분은 다음과 같다.
@AuthenticationPrincipal BoardPrincipal boardPrincipal
Spring Security에서 현재 로그인한 사용자의 인증정보를 전달받는다.
그리고 다음 메서드를 통해 인증정보를 DTO로 변환한다.
articleRequest.toDto(boardPrincipal.toDto())
즉, 게시글 수정 시 사용자 식별정보를 단순히 클라이언트 요청 파라미터에 의존하지 않고, 인증된 사용자의 정보를 이용하도록 구현되어 있다.
이 구조는 사용자가 임의의 사용자 ID를 요청 본문에 넣어 다른 사용자로 가장하는 공격을 방지하는 데 중요하다.
7.2 ArticleService 분석
실제 게시글 수정 로직은 다음과 같다.
public void updateArticle(Long articleId, ArticleDto dto) {
try {
Article article = articleRepository.getReferenceById(articleId);
UserAccount userAccount = userAccountRepository.getReferenceById(
dto.userAccountDto().userId()
);
if (article.getUserAccount().equals(userAccount)) {
if (dto.title() != null) {
article.setTitle(dto.title());
}
if (dto.content() != null) {
article.setContent(dto.content());
}
article.setHashtag(dto.hashtag());
}
} catch (EntityNotFoundException e) {
log.warn("게시글 수정 실패 - {}", e.getLocalizedMessage());
}
}
핵심적인 접근통제 검증 로직은 다음과 같다.
if (article.getUserAccount().equals(userAccount)) {
해당 조건에서는 게시글 작성자와 현재 인증된 사용자가 동일한 경우에만 수정 로직을 실행한다.
따라서 jcob이 jcob2의 게시글인 75번에 대해 수정 요청을 전송하더라도 소유권 비교를 통과하지 못하면 실제 데이터 변경이 이루어지지 않는다.
이번 동적 분석 결과 역시 이러한 코드 흐름과 일치했다.
7.3 개선이 필요한 부분
접근통제 자체는 동작했지만 개선할 부분도 확인했다.
현재 Service에서는 권한이 없는 경우에도 명시적인 예외를 발생시키지 않고 메서드를 종료한다.
Controller는 Service의 처리 결과와 관계없이 다음 응답을 반환한다.
return "redirect:/articles/" + articleId;
따라서 권한이 없는 사용자에게도 정상적인 수정 요청과 동일하게 302 Found 응답이 반환된다.
이 방식은 실제 수정이 차단되더라도 클라이언트가 수정 성공 여부를 명확하게 구분하기 어렵다는 문제가 있다.
개선 방향은 다음과 같다.
- 게시글 소유자와 인증 사용자 정보 불일치 시 명시적 예외 처리
- 권한 없는 수정 요청에 대해 403 Forbidden 반환
- 접근통제 위반에 대한 적절한 보안 로그 기록
- 정상 사용자와 비인가 사용자를 구분하는 자동화 테스트 추가
다만 이번 실습에서는 기존 소스코드를 수정하지 않고 동적·정적 분석을 통해 접근통제 동작을 확인하는 단계까지만 진행했다.
8. 게시글 삭제 기능 접근통제(IDOR) 검증
게시글 수정 기능에 이어 삭제 기능에서도 다른 사용자의 게시글을 임의로 삭제할 수 있는지 확인했다.
8.1 삭제 요청 분석 및 변조
먼저 jcob 계정으로 로그인한 상태에서 본인이 작성한 게시글과 해당 게시글의 삭제 요청을 Burp Suite Proxy에서 확인했다.


이후 해당 요청을 Repeater로 전달하고, URL에 포함된 게시글 ID를 다른 사용자(jcob2)가 작성한 75번 게시글로 변경했다.

변경 전 — 본인 게시글 삭제 요청
POST /articles/{originalArticleId}/delete HTTP/1.1
Host: localhost:8080
Cookie: JSESSIONID=[REDACTED]
변경 후 — 타인 게시글 삭제 요청
POST /articles/75/delete HTTP/1.1
Host: localhost:8080
Cookie: JSESSIONID=[REDACTED]

로그인 계정과 인증정보는 그대로 유지한 상태에서 삭제 대상 게시글 ID만 변경하여 요청을 전송했다.
8.2 삭제 결과 검증
변조한 삭제 요청을 전송한 후 MySQL에서 해당 게시글의 존재 여부를 확인했다.
SELECT id, user_id, title, content
FROM article
WHERE id = 75;
조회 결과 75번 게시글이 그대로 존재했으며, 삭제되지 않은 것을 확인했다.


8.3 소스코드 분석
동적 분석에서 게시글 삭제가 차단된 원인을 확인하기 위해 Controller와 Service의 삭제 처리 로직을 분석했다.
ArticleController.java
@PostMapping("/{articleId}/delete")
public String deleteArticle(
@PathVariable Long articleId,
@AuthenticationPrincipal BoardPrincipal boardPrincipal
) {
articleService.deleteArticle(articleId, boardPrincipal.getUsername());
return "redirect:/articles";
}
Controller에서는 @AuthenticationPrincipal을 통해 현재 로그인한 사용자의 인증정보를 가져온다.
이후 게시글 ID와 인증된 사용자의 ID를 Service 계층으로 전달한다.
articleService.deleteArticle(articleId, boardPrincipal.getUsername());
ArticleService.java
public void deleteArticle(long articleId, String userId) {
articleRepository.deleteByIdAndUserAccount_UserId(articleId, userId);
}
게시글 삭제는 다음 Repository 메서드를 통해 이루어진다.
articleRepository.deleteByIdAndUserAccount_UserId(articleId, userId);
Spring Data JPA의 메서드 이름 기반 쿼리 생성 방식을 사용하고 있으며, 게시글 ID와 작성자 ID를 조건으로 삭제 대상을 지정한다.
개념적으로 다음과 같은 조건에 해당한다.
DELETE FROM article
WHERE id = :articleId
AND user_id = :userId;
※ 위 SQL은 동작 원리를 설명하기 위한 예시이며, 실제 Hibernate가 생성한 SQL은 별도 확인이 필요
즉, 삭제하려는 게시글 ID가 일치하더라도 해당 게시글의 작성자 ID와 로그인한 사용자 ID가 일치하지 않으면 삭제 대상에 포함되지 않는다.
이번 테스트에서는 jcob 계정으로 jcob2의 게시글인 75번에 삭제 요청을 전송했지만, 두 사용자의 ID가 일치하지 않아 삭제가 이루어지지 않은 것으로 판단된다.
수정 기능과 삭제 기능의 차이
수정 기능에서는 Service 계층에서 게시글 작성자와 인증 사용자를 직접 비교하는 방식으로 접근통제를 수행했다.
반면 삭제 기능에서는 Repository 메서드의 조회·삭제 조건에 작성자 ID를 포함하여 접근통제를 수행한다.
두 기능은 구현 방식에 차이가 있지만, 모두 인증된 사용자의 정보를 기준으로 타인 게시글의 비인가 변경을 방지하고 있었다.
다만 삭제 기능에서도 권한이 없는 요청에 대한 명시적인 예외 처리 없이 목록 페이지로 리다이렉트하는 구조이므로, 삭제 권한이 없는 경우 적절한 오류 응답을 제공하도록 개선할 수 있다.
9. 진단 결과 종합
이번 실습에서는 Burp Suite를 활용하여 게시글 수정 및 삭제 기능의 접근통제가 정상적으로 동작하는지 확인했다.
WEB-001 — 타인 게시글 수정 권한 검증
| 항목 |
내용 |
| 진단 대상 | 게시글 수정 기능 |
| 진단 URL | POST /articles/{articleId}/form |
| 진단 유형 | IDOR / 접근통제 검증 |
| 테스트 사용자 | jcob |
| 대상 게시글 | 75번 (jcob2 소유) |
| 진단 방법 | 인증정보 유지 후 게시글 ID 변경 |
| 서버 응답 | 302 Found |
| DB 변경 여부 | 변경되지 않음 |
| 최종 판정 | 양호 (해당 수정 경로 기준) |
진단 결론
인증된 사용자가 다른 사용자의 게시글 ID를 지정하여 수정 요청을 전송했으나, 게시글의 제목 및 내용이 변경되지 않는 것을 확인했다.
소스코드 분석 결과 게시글 작성자와 인증된 사용자 정보를 비교하는 소유권 검증 로직이 존재했으며, 해당 로직이 이번 접근통제 테스트에서 정상적으로 동작한 것으로 판단된다.
단, 권한 없는 수정 요청에 대해 명시적인 오류를 반환하지 않고 302 Found로 리다이렉트하는 부분은 개선이 필요하다.
WEB-002 — 타인 게시글 삭제 권한 검증
| 항목 | 내용 |
| 진단 대상 | 게시글 삭제 기능 |
| 진단 URL | POST /articles/{articleId}/delete |
| 진단 유형 | IDOR / 접근통제 검증 |
| 테스트 사용자 | jcob |
| 대상 게시글 | 75번 (jcob2 소유) |
| 진단 방법 | 인증정보 유지 후 게시글 ID 변경 |
| DB 삭제 여부 | 삭제되지 않음 |
| 최종 판정 | 양호 (해당 삭제 경로 기준) |
진단 결론
인증된 사용자가 다른 사용자의 게시글 ID를 지정하여 삭제 요청을 전송했으나, 데이터베이스에서 해당 게시글이 삭제되지 않은 것을 확인했다.
소스코드 분석 결과 게시글 삭제 시 게시글 ID뿐 아니라 인증된 사용자의 ID를 함께 조건으로 사용하는 로직이 존재했다.
따라서 이번 테스트에서는 타인 게시글 삭제로 이어지는 IDOR 취약점이 재현되지 않았다.
다만 삭제 권한이 없는 요청에 대한 명시적인 예외 처리는 개선이 필요하다.
종합 판정
이번에 테스트한 게시글 수정 및 삭제 기능에서는 모두 다른 사용자의 게시글에 대한 비인가 변경이 차단되는 것을 확인했다.
해당 결과는 테스트한 요청 경로를 기준으로 한 판정이며, 게시판의 모든 기능에 대한 접근통제가 안전하다는 의미는 아니다.
10. 마무리 및 향후 분석 계획
이번 진단에서는 Docker로 구축한 Spring Boot 게시판에 Burp Suite를 연결하고, 게시글 수정 및 삭제 요청을 직접 변조하여 접근통제 동작을 검증했다.
특히 서버가 반환하는 HTTP 상태 코드만으로 성공 여부를 판단하지 않고, 실제 데이터베이스의 변경 결과와 Controller·Service 소스코드를 함께 확인했다.
이를 통해 다음 내용을 확인할 수 있었다.
- Burp Suite를 이용한 HTTP 요청·응답 분석
- Repeater를 이용한 게시글 ID 변조
- 정상 사용자와 비인가 사용자의 요청 비교
- MySQL을 통한 실제 데이터 변경 여부 검증
- Spring Security 인증정보와 서버 측 소유권 검증 구조 분석
- Service 및 Repository 계층에서의 접근통제 구현 방식 비교
이번에는 게시글 수정 및 삭제 기능에서 IDOR 취약점이 발견되지 않았지만, 접근통제 검증 로직이 실제로 어떤 방식으로 동작하는지 확인했다는 점에서 의미가 있었다.
다음 단계에서는 댓글 기능의 접근통제를 검증하고, 이후 XSS 등 입력값 처리 취약점으로 분석 범위를 확대할 예정이다.
'Web Hacking > Web Security Projects' 카테고리의 다른 글
| [웹 모의해킹 프로젝트 #01] Spring Boot 게시판 Docker 환경 구축 및 초기 데이터 설정 (0) | 2026.10.24 |
|---|
















































































