Conversation
- 공통 요소 반영을 위해 DynamicViewResponse를 반환하는 기본 뷰 핸들러 추가
- 해더에 "글쓰기" 버튼 추가 - 헤더의 닉네임에 /mypage 리다이렉트 기능 추가
- `/static` 하위에 있던 정적 뷰들을 `/templates`하위로 이동 - 이동한 정적 뷰들에 공통 컴포넌트 반영
| 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; | ||
| } | ||
|
|
||
| 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; | ||
| } | ||
|
|
||
| let err = null; | ||
| try { err = await res.json(); } catch {} |
There was a problem hiding this comment.
XSS 취약점: msgEl.textContent = message를 사용하고 있는 것은 좋지만, 문제는 message가 서버 응답에서 온 데이터인 경우 유효성 검증이 없다는 점입니다. 또한 form.action을 fetch()의 대상으로 사용하는데, form.action이 신뢰할 수 없는 출처인 경우 CSRF 위험이 있습니다.
제안:
- 폼의
action속성이 같은 출처(same-origin)인지 명시적으로 검증 credentials: "same-origin"만으로는 부족하므로, URL의 호스트/프로토콜 일치 확인 추가
예시:
const formUrl = new URL(form.action, window.location.origin);
if (formUrl.origin !== window.location.origin) {
window.showErrorPopup("유효하지 않은 폼 대상입니다.", "오류");
return;
}| }); | ||
|
|
||
| if (res.ok) { | ||
| // 성공 처리: 서버가 리다이렉트를 주는 구조면 여기서 location 설정 | ||
| // 우선 가장 단순하게는 reload 혹은 홈 이동 | ||
| window.location.href = "/"; | ||
| return; | ||
| } |
There was a problem hiding this comment.
폼 제출 후 처리 로직 불명확: res.ok인 경우 무조건 window.location.href = "/"로 이동시키는데, 이는 모든 AJAX 폼 제출에 동일하게 적용됩니다. 로그인, 회원가입, 댓글 등 다양한 폼이 있을 텐데, 성공 후 이동 경로가 고정되어 있어 유연성이 없습니다.
제안: 폼에 data-redirect-url 같은 속성을 추가하여 성공 후 이동 경로를 명시적으로 지정하거나, 서버에서 리다이렉트 정보를 응답에 포함시키세요.
| 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()); | ||
| }); |
There was a problem hiding this comment.
디렉토리 트래버설(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; // 경로 정규화 실패
}| public HandlerResponse handle(HttpRequest request) { | ||
| String path = request.getPath() + (request.getPath().endsWith("/") ? "index.html" : "/index.html"); | ||
| return DynamicViewResponse.of(HttpStatus.OK, path); | ||
| } |
There was a problem hiding this comment.
인덱스 파일 로직 중복: handle() 메서드에서 경로에 /index.html을 추가하는데, checkEndpoint()에서 이미 인덱스 파일 존재 여부를 확인했습니다. 이 두 메서드 간 로직이 일치하지 않으면 버그가 발생할 수 있습니다. 예를 들어, checkEndpoint()에서 /index.html을 찾았다면, handle()에서 다시 /index.html을 추가할 때 중복되지 않도록 주의해야 합니다.
제안: 경로 정규화 로직을 한 곳(예: static 메서드)에 집중시키세요.
|
|
||
| e.preventDefault(); | ||
|
|
||
| // multipart/form-data는 여기서 처리 안 함(파일 업로드 등) | ||
| const enctype = (form.enctype || "").toLowerCase(); | ||
| if (enctype.includes("multipart/form-data")) { | ||
| window.showErrorPopup("파일 업로드 폼은 지원하지 않습니다.", "오류"); | ||
| return; |
There was a problem hiding this comment.
폼 필터링 문제: if (form.dataset.ajax !== "true") return로 AJAX 처리 여부를 결정하는데, 이렇게 하면 data-ajax 속성이 없는 폼은 기본 form submit 동작을 합니다. 만약 템플릿 변경 시 실수로 data-ajax="true"를 빠뜨리면, AJAX 없이 전체 페이지 새로고침이 되어 사용 경험이 다음 수 있습니다.
제안: AJAX 처리 대상 폼을 명시적으로 관리하거나, 특정 클래스(예: ajax-form)로 식별하는 방식으로 변경하여 기본 동작과 명확히 구분하세요.
| <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> |
There was a problem hiding this comment.
스타일 일관성 문제: 닉네임을 <span>에서 <a> 태그로 변경했는데, CSS 스타일이 수정되지 않으면 링크 스타일(파란색, 밑줄 등)이 헤더 디자인을 깨뜨릴 수 있습니다. 기존 .header__menu__nickname 스타일이 <span> 기준이었다면, <a> 태그에 적용할 때 text-decoration: none, color 등을 명시적으로 재정의해야 합니다.
제안: CSS 파일에서 .header__menu__nickname { text-decoration: none; color: inherit; } 등을 추가하거나, 링크 스타일을 정의하세요.
|
|
||
| // multipart/form-data는 여기서 처리 안 함(파일 업로드 등) | ||
| const enctype = (form.enctype || "").toLowerCase(); | ||
| if (enctype.includes("multipart/form-data")) { | ||
| window.showErrorPopup("파일 업로드 폼은 지원하지 않습니다.", "오류"); | ||
| return; |
There was a problem hiding this comment.
멀티파트 폼 제한 사항: enctype.includes("multipart/form-data")인 폼을 지원하지 않는다고 명시되어 있으나, 현재 코드는 에러 메시지만 표시합니다. 실제로 파일 업로드가 필요한 경우 (예: 프로필 이미지 변경), 사용자가 AJAX 제출을 시도했을 때 에러가 발생합니다.
제안:
- 멀티파트 폼은 전통적인 form submit을 허용하거나
- FormData 기반 AJAX로 파일 업로드를 지원하도록 개선하세요
이 부분은 mypage의 프로필 이미지 업로드 기능과 연관될 수 있으므로 주의가 필요합니다.
- 서버의 반환값을 받아 redirect면 Location으로 리다이렉트 되도록 분기 코드 추가
develop <- enhance/view/#56
💻 작업 내용
/static->/templates하위로 이동✨ 리뷰 포인트
뷰의 공통 요소에 취약점이 있는지 체크 부탁합니다.
🎯 관련 이슈
closed #56