Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
4 changes: 2 additions & 2 deletions src/main/java/app/handler/RegisterWithPost.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import web.filter.authentication.UserRole;
import web.handler.SingleArgHandler;
import web.response.HandlerResponse;
import web.response.StaticViewResponse;
import web.response.RedirectResponse;

public class RegisterWithPost extends SingleArgHandler<QueryParameters> {
private static final Logger log = LoggerFactory.getLogger(RegisterWithPost.class);
Expand All @@ -27,6 +27,6 @@ public HandlerResponse handle(QueryParameters params) {
String password = params.getQueryValue("password").orElseThrow(()-> new ServiceException(ErrorCode.MISSING_REGISTER_TOKEN, "password required"));
Database.addUser(new User(password, nickname, email, UserRole.MEMBER.toString()));
log.info("Registered - password:{}, nickname:{}, email:{}", password, nickname, email);
return StaticViewResponse.of("/login");
return RedirectResponse.to("/login");
}
}
8 changes: 7 additions & 1 deletion src/main/java/config/AppConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import web.dispatch.argument.resolver.HttpRequestResolver;
import web.dispatch.argument.resolver.QueryParamsResolver;
import web.filter.*;
import web.handler.DefaultViewHandler;
import web.handler.StaticContentHandler;
import web.handler.WebHandler;
import web.renderer.DynamicViewRenderer;
Expand Down Expand Up @@ -88,7 +89,8 @@ public List<WebHandler> webHandlerList() {
registerWithPost(),
loginWithPost(),
logoutWithPost(),
homeHandler())
homeHandler(),
defaultViewHandler())
);
}

Expand All @@ -99,6 +101,10 @@ public StaticContentHandler staticContentHandler() {
);
}

public DefaultViewHandler defaultViewHandler(){
return getOrCreate("defaultViewHandler", DefaultViewHandler::new);
}

public RegisterWithGet registerWithGet() {
return getOrCreate(
"registerWithGet",
Expand Down
43 changes: 43 additions & 0 deletions src/main/java/web/handler/DefaultViewHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package web.handler;

import config.VariableConfig;
import exception.ErrorException;
import http.HttpMethod;
import http.HttpStatus;
import http.request.HttpRequest;
import web.response.DynamicViewResponse;
import web.response.HandlerResponse;

import java.io.File;
import java.util.List;

public class DefaultViewHandler implements DefaultHandler{
private final List<String> roots = VariableConfig.DYNAMIC_RESOURCE_ROOTS;
private final HttpMethod method = HttpMethod.GET;

@Override
public String getPath() {
throw new ErrorException("DynamicViewHandler::getPath should not be called");
}

@Override
public HttpMethod getMethod() {
return this.method;
}
@Override
public boolean checkEndpoint(HttpMethod method, String path) {
if(!method.equals(this.method)) return false;
return roots.stream().anyMatch(root ->{
File requestedFile = new File(root + path);
String indexFilePath = path + (path.endsWith("/") ? "index.html" : "/index.html");
File indexFile = new File(root + indexFilePath);
return (requestedFile.exists() && requestedFile.isFile()) || (indexFile.exists() && indexFile.isFile());
});
Comment on lines +28 to +35

Choose a reason for hiding this comment

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

디렉토리 트래버설(Path Traversal) 위험: checkEndpoint() 메서드에서 경로 검증이 불충분합니다. 사용자가 ../../../etc/passwd 같은 경로를 요청할 경우, new File(root + path).exists() 검사만으로는 보호되지 않습니다. (Java의 File API는 심볼릭 링크나 경로 정규화 문제가 있을 수 있음)

제안:

try {
  File requestedFile = new File(root + path).getCanonicalFile();
  File rootFile = new File(root).getCanonicalFile();
  if (!requestedFile.getAbsolutePath().startsWith(rootFile.getAbsolutePath())) {
    return false; // 루트 범위 밖
  }
} catch (IOException e) {
  return false; // 경로 정규화 실패
}

}

@Override
public HandlerResponse handle(HttpRequest request) {
String path = request.getPath() + (request.getPath().endsWith("/") ? "index.html" : "/index.html");
return DynamicViewResponse.of(HttpStatus.OK, path);
}
Comment on lines +39 to +42

Choose a reason for hiding this comment

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

인덱스 파일 로직 중복: handle() 메서드에서 경로에 /index.html을 추가하는데, checkEndpoint()에서 이미 인덱스 파일 존재 여부를 확인했습니다. 이 두 메서드 간 로직이 일치하지 않으면 버그가 발생할 수 있습니다. 예를 들어, checkEndpoint()에서 /index.html을 찾았다면, handle()에서 다시 /index.html을 추가할 때 중복되지 않도록 주의해야 합니다.

제안: 경로 정규화 로직을 한 곳(예: static 메서드)에 집중시키세요.

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,12 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="../reset.css" rel="stylesheet" />
<link href="../global.css" rel="stylesheet" />
<link href="../../static/reset.css" rel="stylesheet" />
<link href="../../static/global.css" rel="stylesheet" />
</head>
<body>
<div class="container">
<header class="header">
<a href="/main"><img src="../img/signiture.svg" /></a>
<ul class="header__menu">
<li class="header__menu__item">
<a class="btn btn_contained btn_size_s" href="/article">글쓰기</a>
</li>
<li class="header__menu__item">
<button id="logout-btn" class="btn btn_ghost btn_size_s">
로그아웃
</button>
</li>
</ul>
</header>
{{> /layout/header.html}}
<div class="page">
<h2 class="page-title">게시글 작성</h2>
<form class="form">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,12 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="../reset.css" rel="stylesheet" />
<link href="../global.css" rel="stylesheet" />
<link href="../../static/reset.css" rel="stylesheet" />
<link href="../../static/global.css" rel="stylesheet" />
</head>
<body>
<div class="container">
<header class="header">
<a href="/main"><img src="../img/signiture.svg" /></a>
<ul class="header__menu">
<li class="header__menu__item">
<a class="btn btn_contained btn_size_s" href="/article">글쓰기</a>
</li>
<li class="header__menu__item">
<button id="logout-btn" class="btn btn_ghost btn_size_s">
로그아웃
</button>
</li>
</ul>
</header>
{{> /layout/header.html}}
<div class="page">
<h2 class="page-title">댓글 작성</h2>
<form class="form">
Expand Down
120 changes: 120 additions & 0 deletions src/main/resources/templates/layout/error-popup.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<style>
.error-popup-wrapper {
position: fixed;
top: 16px;
right: 50px;
z-index: 1000;
}
.error-popup {
display: none;
background: #ffffff;
border-radius: 8px;
padding: 14px 18px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
min-width: 260px;
}
.error-popup__title {
font-size: 14px;
font-weight: 600;
margin-bottom: 6px;
color: #d32f2f;
}
.error-popup__message {
font-size: 13px;
margin-bottom: 10px;
}
.error-popup__actions {
display: flex;
justify-content: flex-end;
}
.error-popup__ok-btn {
cursor: pointer;
padding: 4px 12px;
font-size: 13px;
border-radius: 4px;
border: none;
}
</style>

<div class="error-popup-wrapper">
<div id="error-popup" class="error-popup">
<div class="error-popup__title">알림</div>
<div id="error-popup-message" class="error-popup__message"></div>
<div class="error-popup__actions">
<button id="error-popup-ok" class="error-popup__ok-btn" type="button">
OK
</button>
</div>
</div>
</div>

<script>
(function () {
const popup = document.getElementById("error-popup");
const msgEl = document.getElementById("error-popup-message");
const okBtn = document.getElementById("error-popup-ok");

window.showErrorPopup = function (message, title) {
const titleEl = popup.querySelector(".error-popup__title");
msgEl.textContent = message || "오류가 발생했습니다.";
if (title) titleEl.textContent = title;
popup.style.display = "block";
};

window.hideErrorPopup = function () {
popup.style.display = "none";
};

okBtn.addEventListener("click", () => window.hideErrorPopup());

// ---- 공통 submit 가로채기 (B안: data-ajax 있는 폼만) ----
document.addEventListener("submit", async (e) => {
const form = e.target;
if (!(form instanceof HTMLFormElement)) return;

// 안전하게: data-ajax="true"인 폼만 가로챔
if (form.dataset.ajax !== "true") return;

e.preventDefault();

// multipart/form-data는 여기서 처리 안 함(파일 업로드 등)
const enctype = (form.enctype || "").toLowerCase();
if (enctype.includes("multipart/form-data")) {
window.showErrorPopup("파일 업로드 폼은 지원하지 않습니다.", "오류");
return;
Comment on lines +77 to +84

Choose a reason for hiding this comment

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

폼 필터링 문제: if (form.dataset.ajax !== "true") return로 AJAX 처리 여부를 결정하는데, 이렇게 하면 data-ajax 속성이 없는 폼은 기본 form submit 동작을 합니다. 만약 템플릿 변경 시 실수로 data-ajax="true"를 빠뜨리면, AJAX 없이 전체 페이지 새로고침이 되어 사용 경험이 다음 수 있습니다.

제안: AJAX 처리 대상 폼을 명시적으로 관리하거나, 특정 클래스(예: ajax-form)로 식별하는 방식으로 변경하여 기본 동작과 명확히 구분하세요.

Comment on lines +79 to +84

Choose a reason for hiding this comment

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

멀티파트 폼 제한 사항: enctype.includes("multipart/form-data")인 폼을 지원하지 않는다고 명시되어 있으나, 현재 코드는 에러 메시지만 표시합니다. 실제로 파일 업로드가 필요한 경우 (예: 프로필 이미지 변경), 사용자가 AJAX 제출을 시도했을 때 에러가 발생합니다.

제안:

  1. 멀티파트 폼은 전통적인 form submit을 허용하거나
  2. FormData 기반 AJAX로 파일 업로드를 지원하도록 개선하세요

이 부분은 mypage의 프로필 이미지 업로드 기능과 연관될 수 있으므로 주의가 필요합니다.

}

const body = new URLSearchParams(new FormData(form));

try {
const res = await fetch(form.action, {
method: (form.method || "POST").toUpperCase(),
headers: {
"Accept": "application/json",
"Content-Type": "application/x-www-form-urlencoded",
},
body,
credentials: "same-origin",
});

if (res.ok) {
// 성공 처리: 서버가 리다이렉트를 주는 구조면 여기서 location 설정
// 우선 가장 단순하게는 reload 혹은 홈 이동
window.location.href = "/";
return;
}
Comment on lines 98 to 106

Choose a reason for hiding this comment

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

폼 제출 후 처리 로직 불명확: res.ok인 경우 무조건 window.location.href = "/"로 이동시키는데, 이는 모든 AJAX 폼 제출에 동일하게 적용됩니다. 로그인, 회원가입, 댓글 등 다양한 폼이 있을 텐데, 성공 후 이동 경로가 고정되어 있어 유연성이 없습니다.

제안: 폼에 data-redirect-url 같은 속성을 추가하여 성공 후 이동 경로를 명시적으로 지정하거나, 서버에서 리다이렉트 정보를 응답에 포함시키세요.


let err = null;
try { err = await res.json(); } catch {}
Comment on lines 71 to 109

Choose a reason for hiding this comment

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

XSS 취약점: msgEl.textContent = message를 사용하고 있는 것은 좋지만, 문제는 message가 서버 응답에서 온 데이터인 경우 유효성 검증이 없다는 점입니다. 또한 form.actionfetch()의 대상으로 사용하는데, form.action이 신뢰할 수 없는 출처인 경우 CSRF 위험이 있습니다.

제안:

  1. 폼의 action 속성이 같은 출처(same-origin)인지 명시적으로 검증
  2. credentials: "same-origin"만으로는 부족하므로, URL의 호스트/프로토콜 일치 확인 추가

예시:

const formUrl = new URL(form.action, window.location.origin);
if (formUrl.origin !== window.location.origin) {
  window.showErrorPopup("유효하지 않은 폼 대상입니다.", "오류");
  return;
}


const message =
(err && (err.message || err.errorMessage)) ||
"요청 처리에 실패했습니다.";

window.showErrorPopup(message, "요청 실패");
} catch (networkError) {
window.showErrorPopup("서버와 통신 중 문제가 발생했습니다.", "오류");
}
}, true); // capture로 잡으면 더 안정적으로 submit을 가로챌 수 있음
})();
</script>
5 changes: 4 additions & 1 deletion src/main/resources/templates/layout/header.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
<ul class="header__menu">
{{#if1 isLoginUser}}
<li class="header__menu__item">
<span class="header__menu__nickname">닉네임: {{userNickname}}</span>
<a class="header__menu__nickname" href="/mypage">닉네임: {{userNickname}}</a>
</li>
<li class="header__menu__item">
<a class="btn btn_contained btn_size_s" href="/article">글쓰기</a>
</li>
Comment on lines 6 to 11

Choose a reason for hiding this comment

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

스타일 일관성 문제: 닉네임을 <span>에서 <a> 태그로 변경했는데, CSS 스타일이 수정되지 않으면 링크 스타일(파란색, 밑줄 등)이 헤더 디자인을 깨뜨릴 수 있습니다. 기존 .header__menu__nickname 스타일이 <span> 기준이었다면, <a> 태그에 적용할 때 text-decoration: none, color 등을 명시적으로 재정의해야 합니다.

제안: CSS 파일에서 .header__menu__nickname { text-decoration: none; color: inherit; } 등을 추가하거나, 링크 스타일을 정의하세요.

<form action="/user/logout" method="POST">
<button type="submit">로그아웃</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,15 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="../reset.css" rel="stylesheet" />
<link href="../global.css" rel="stylesheet" />
<link href="../../static/reset.css" rel="stylesheet" />
<link href="../../static/global.css" rel="stylesheet" />
</head>
<body>
<div class="container">
<header class="header">
<a href="/"><img src="../img/signiture.svg" /></a>
<ul class="header__menu">
<li class="header__menu__item">
<a class="btn btn_contained btn_size_s" href="/login">로그인</a>
</li>
<li class="header__menu__item">
<a class="btn btn_ghost btn_size_s" href="/registration">
회원 가입
</a>
</li>
</ul>
</header>
{{> /layout/header.html}}
<div class="page">
<h2 class="page-title">로그인</h2>
<form class="form" action="/user/login" method="post">
<form class="form" action="/user/login" method="post" data-ajax="true">
<div class="textfield textfield_size_s">
<p class="title_textfield">이메일</p>
<input
Expand Down Expand Up @@ -54,5 +42,6 @@ <h2 class="page-title">로그인</h2>
</form>
</div>
</div>
{{> /layout/error-popup.html}}
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,13 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="../reset.css" rel="stylesheet" />
<link href="../global.css" rel="stylesheet" />
<link href="../../static/reset.css" rel="stylesheet" />
<link href="../../static/global.css" rel="stylesheet" />
</head>
<body>
<div class="container">
<header class="header">
<a href="/"><img src="../img/signiture.svg" /></a>
<ul class="header__menu">
<li class="header__menu__item">
<a class="btn btn_contained btn_size_s" href="/article">글쓰기</a>
</li>
<li class="header__menu__item">
<button id="logout-btn" class="btn btn_ghost btn_size_s">
로그아웃
</button>
</li>
</ul>
</header>
{{> /layout/header.html}}

<div class="page">
<h2 class="page-title">마이페이지</h2>

Expand Down Expand Up @@ -74,8 +63,10 @@ <h2 class="page-title">마이페이지</h2>
변경사항 저장
</button>

</form>
</div>
</form>
</div>
</div>
</div>
{{> /layout/error-popup.html}}
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,15 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="../reset.css" rel="stylesheet" />
<link href="../global.css" rel="stylesheet" />
<link href="../../static/reset.css" rel="stylesheet" />
<link href="../../static/global.css" rel="stylesheet" />
</head>
<body>
<div class="container">
<header class="header">
<a href="/"><img src="../img/signiture.svg" /></a>
<ul class="header__menu">
<li class="header__menu__item">
<a class="btn btn_contained btn_size_s" href="/login">로그인</a>
</li>
<li class="header__menu__item">
<a class="btn btn_ghost btn_size_s" href="/registration">
회원 가입
</a>
</li>
</ul>
</header>
{{> /layout/header.html}}
<div class="page">
<h2 class="page-title">회원가입</h2>
<form class="form" action="/user/create" method="post">
<form class="form" action="/user/create" method="post" data-ajax="true">
<div class="textfield textfield_size_s">
<p class="title_textfield">이메일</p>
<input
Expand Down Expand Up @@ -63,5 +51,6 @@ <h2 class="page-title">회원가입</h2>
</form>
</div>
</div>
{{> /layout/error-popup.html}}
</body>
</html>