겉바속촉

[보안] 파일 업로드 취약점 방어 본문

IT 일기 (상반기)/네트워크 및 시스템 보안

[보안] 파일 업로드 취약점 방어

겉바속촉 2021. 2. 12. 03:34
728x90
반응형

 

 

파일 업로드 취약점 방어 기법

 

 

  1. 업로드 파일의 크기와 개수를 제한
  2. 업로드 파일의 종류를 제한
  3. 외부에서 접근할 수 없는 경로에 업로드 파일을 저장 => 웹 (도큐먼트) 루트 밖에 저장
  4. 업로드 파일의 이름과 저장 경로를 외부에서 알 수 없도록 변경해서 저장
  5. 업로드 파일의 실행 속성을 제거하고 저장

 

 

파일 업로드 취약점이 제대로 방어된다면 다운로드 기능이 필요해 질 것입니다.

 

 

 

 

 

**웹 도큐먼트 루트는 지난 포스팅 참고**

 

2021/02/11 - [IT 일기 (상반기)/네트워크 및 시스템 보안] - [보안] 웹 도큐먼트 루트

 

[보안] 웹 도큐먼트 루트

지난번에 파일 업로드 취약점에 대해 알아봤습니다. 2021/02/11 - [IT 일기 (상반기)/네트워크 및 시스템 보안] - [보안] 파일 업로드 취약점 [보안] 파일 업로드 취약점 파일 업로드 취약점 (= 위험한

2-juhyun-2.tistory.com

 

 

 

 

1. 취약한 코드 확인

 

@Winxp - Eclipse - Ctrl+Shift+R로 다음 파일을 찾아주세요.

 

 

전체 코드는 다음과 같습니다.

package kr.co.openeg.lab.board.controller;

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

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import kr.co.openeg.lab.board.model.BoardCommentModel;
import kr.co.openeg.lab.board.model.BoardModel;
import kr.co.openeg.lab.board.service.BoardService;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping("/board")
public class BoardController {

	@Resource(name = "boardService")
	private BoardService service;

	private int currentPage = 1;
	private int showArticleLimit = 10;
	private int showPageLimit = 10;
	private int startArticleNum = 0;
	private int endArticleNum = 0;
	private int totalNum = 0;

	@RequestMapping("/list.do")
	public ModelAndView boardList(HttpServletRequest request, HttpServletResponse response) {

		String type = null;
		String keyword = null;

		// set variables from request parameter
		if (request.getParameter("page") == null || request.getParameter("page").trim().isEmpty() || request.getParameter("page").equals("0")) {
			currentPage = 1;
		} else {
			currentPage = Integer.parseInt(request.getParameter("page"));
		}

		if (request.getParameter("type") != null) {
			type = request.getParameter("type").trim();
		}

		if (request.getParameter("keyword") != null) {
			keyword = request.getParameter("keyword").trim();
		}
		//

		// expression article variables value
		startArticleNum = (currentPage - 1) * showArticleLimit + 1;
		endArticleNum = startArticleNum + showArticleLimit - 1;
		//

		// get boardList and get page html code
		List<BoardModel> boardList;
		if (type != null && keyword != null) {
			boardList = service.searchArticle(type, keyword, startArticleNum, endArticleNum);
			totalNum = service.getSearchTotalNum(type, keyword);
		} else {
			boardList = service.getBoardList(startArticleNum, endArticleNum);
			totalNum = service.getTotalNum();
		}
		StringBuffer pageHtml = getPageHtml(currentPage, totalNum, showArticleLimit, showPageLimit, type, keyword);
		//

		ModelAndView mav = new ModelAndView();
		mav.addObject("boardList", boardList);
		mav.addObject("pageHtml", pageHtml);
		mav.setViewName("/board/list");

		return mav;
	}

	// A method for Creating page html code
	private StringBuffer getPageHtml(int currentPage, int totalNum, int showArticleLimit, int showPageLimit, String type, String keyword) {
		StringBuffer pageHtml = new StringBuffer();
		int startPage = 0;
		int lastPage = 0;

		// expression page variables
		startPage = ((currentPage - 1) / showPageLimit) * showPageLimit + 1;
		lastPage = startPage + showPageLimit - 1;

		if (lastPage > totalNum / showArticleLimit) {
			lastPage = (totalNum / showArticleLimit) + 1;
		}
		//

		// create page html code
		// if: when no search
		if (type == null && keyword == null) {
			if (currentPage == 1) {
				pageHtml.append("<span>");
			} else {
				pageHtml.append("<span><a href=\"list.do?page=" + (currentPage - 1) + "\"><이전></a>&nbsp;&nbsp;");
			}

			for (int i = startPage; i <= lastPage; i++) {
				if (i == currentPage) {
					pageHtml.append(".&nbsp;<strong>");
					pageHtml.append("<a href=\"list.do?page=" + i + "\" class=\"page\">" + i + "</a>");
					pageHtml.append("&nbsp;</strong>");
				} else {
					pageHtml.append(".&nbsp;<a href=\"list.do?page=" + i + "\" class=\"page\">" + i + "</a>&nbsp;");
				}

			}
			if (currentPage == lastPage) {
				pageHtml.append(".</span>");
			} else {
				pageHtml.append(".&nbsp;&nbsp;<a href=\"list.do?page=" + (currentPage + 1) + "\"><다음></a></span>");
			}
			//
			// else: when search
		} else {
			if (currentPage == 1) {
				pageHtml.append("<span>");
			} else {
				pageHtml.append("<span><a href=\"list.do?page=" + (currentPage - 1) + "&type=" + type + "&keyword=" + keyword + "\"><이전></a>&nbsp;&nbsp;");
			}

			for (int i = startPage; i <= lastPage; i++) {
				if (i == currentPage) {
					pageHtml.append(".&nbsp;<strong>");
					pageHtml.append("<a href=\"list.do?page=" + i + "&type=" + type + "&keyword=" + keyword + "\" class=\"page\">" + i + "</a>&nbsp;");
					pageHtml.append("&nbsp;</strong>");
				} else {
					pageHtml.append(".&nbsp;<a href=\"list.do?page=" + i + "&type=" + type + "&keyword=" + keyword + "\" class=\"page\">" + i + "</a>&nbsp;");
				}

			}
			if (currentPage == lastPage) {
				pageHtml.append("</span>");
			} else {
				pageHtml.append(".&nbsp;&nbsp;<a href=\"list.do?page=" + (currentPage + 1) + "&type=" + type + "&keyword=" + keyword + "\"><다음></a></span>");
			}
		}
		//
		return pageHtml;
	}

	@RequestMapping("/view.do")
	public ModelAndView boardView(HttpServletRequest request) {
		int idx = Integer.parseInt(request.getParameter("idx"));
		BoardModel board = service.getOneArticle(idx);

		service.updateHitcount(board.getHitcount() + 1, idx);

		List<BoardCommentModel> commentList = service.getCommentList(idx);

		ModelAndView mav = new ModelAndView();
		mav.addObject("board", board);
		mav.addObject("commentList", commentList);
		mav.setViewName("/board/view");
		return mav;
	}

	@RequestMapping("/write.do")
	public String boardWrite(@ModelAttribute("BoardModel") BoardModel boardModel, HttpSession session) {
		return "/board/write";
	}

	@RequestMapping(value = "/write.do", method = RequestMethod.POST)
	public String boardWriteProc(@ModelAttribute("BoardModel") BoardModel boardModel, MultipartHttpServletRequest request, HttpSession session) {
		String uploadPath = session.getServletContext().getRealPath("/") + "files/";
		File dir = new File(uploadPath);
		if (!dir.exists()) {
			dir.mkdir();
		}

		MultipartFile file = request.getFile("file");
		if (file != null && !"".equals(file.getOriginalFilename())) {
			String fileName = file.getOriginalFilename();
			File uploadFile = new File(uploadPath + fileName);
			if (uploadFile.exists()) {
				fileName = new Date().getTime() + fileName;
				uploadFile = new File(uploadPath + fileName);
			}

			try {
				file.transferTo(uploadFile);
			} catch (Exception e) {
				System.out.println("upload error");
			}
						
			boardModel.setFileName(fileName);
		}

		String content = boardModel.getContent().replaceAll("\r\n", "<br />");
		boardModel.setContent(content);
		service.writeArticle(boardModel);

		return "redirect:list.do";
	}

	@RequestMapping("/commentWrite.do")
	public ModelAndView commentWriteProc(@ModelAttribute("CommentModel") BoardCommentModel commentModel) {
		// new line code change to <br /> tag
		String content = commentModel.getContent().replaceAll("\r\n", "<br />");
		commentModel.setContent(content);
		//
		service.writeComment(commentModel);
		ModelAndView mav = new ModelAndView();
		mav.addObject("idx", commentModel.getLinkedArticleNum());
		mav.setViewName("redirect:view.do");

		return mav;
	}

	@RequestMapping("/modify.do")
	public ModelAndView boardModify(HttpServletRequest request, HttpSession session) {
		String userId = (String) session.getAttribute("userId");
		int idx = Integer.parseInt(request.getParameter("idx"));

		BoardModel board = service.getOneArticle(idx);
		String content = board.getContent().replaceAll("<br />", "\r\n");
		board.setContent(content);

		ModelAndView mav = new ModelAndView();
		if (!userId.equals(board.getWriterId())) {
			mav.addObject("errCode", "1"); // forbidden connection
			mav.addObject("idx", idx);
			mav.setViewName("redirect:view.do");
		} else {
			mav.addObject("board", board);
			mav.setViewName("/board/modify");
		}

		return mav;
	}

	@RequestMapping(value = "/modify.do", method = RequestMethod.POST)
	public ModelAndView boardModifyProc(@ModelAttribute("BoardModel") BoardModel boardModel, MultipartHttpServletRequest request) {
		String uploadPath = request.getContextPath() + "/files/";
		String orgFileName = request.getParameter("orgFile");
		MultipartFile newFile = request.getFile("newFile");
		String newFileName = newFile.getOriginalFilename();

		boardModel.setFileName(orgFileName);

		// if: when want to change upload file
		if (newFile != null && !newFileName.equals("")) {
			if (orgFileName != null || !orgFileName.equals("")) {
				// remove uploaded file
				File removeFile = new File(uploadPath + orgFileName);
				removeFile.delete();
				//
			}
			// create new upload file
			File newUploadFile = new File(uploadPath + newFileName);
			try {
				newFile.transferTo(newUploadFile);
			} catch (Exception e) {
				e.printStackTrace();
			}
			//
			boardModel.setFileName(newFileName);
		}
		//
		// new line code change to <br /> tag
		String content = boardModel.getContent().replaceAll("\r\n", "<br />");
		boardModel.setContent(content);
		//

		service.modifyArticle(boardModel);

		ModelAndView mav = new ModelAndView();
		mav.addObject("idx", boardModel.getIdx());
		mav.setViewName("redirect:/board/view.do");
		return mav;
	}

	@RequestMapping("/delete.do")
	public ModelAndView boardDelete(HttpServletRequest request, HttpSession session) {
		String uploadPath = request.getContextPath() + "/files/";
		System.out.println("uploadPath: " + uploadPath);
		String userId = (String) session.getAttribute("userId");
		int idx = Integer.parseInt(request.getParameter("idx"));

		BoardModel board = service.getOneArticle(idx);

		ModelAndView mav = new ModelAndView();

		if (!userId.equals(board.getWriterId())) {
			mav.addObject("errCode", "1"); // it's forbidden connection
			mav.addObject("idx", idx);
			mav.setViewName("redirect:view.do");
		} else {
			List<BoardCommentModel> commentList = service.getCommentList(idx);
			if (commentList.size() > 0) {
				mav.addObject("errCode", "2");
				mav.addObject("idx", idx);
				mav.setViewName("redirect:view.do");
			} else {
				// if: when the article has upload file - remove that
				if (board.getFileName() != null) {
					File removeFile = new File(uploadPath + board.getFileName());
					removeFile.delete();
				}
				//
				service.deleteArticle(idx);

				mav.setViewName("redirect:list.do");
			}
		}
		return mav;
	}

	@RequestMapping("/commentDelete.do")
	public ModelAndView commendDelete(HttpServletRequest request, HttpSession session) {
		int idx = Integer.parseInt(request.getParameter("idx"));
		int linkedArticleNum = Integer.parseInt(request.getParameter("linkedArticleNum"));

		String userId = (String) session.getAttribute("userId");
		BoardCommentModel comment = service.getOneComment(idx);

		ModelAndView mav = new ModelAndView();

		if (!userId.equals(comment.getWriterId())) {
			mav.addObject("errCode", "1");
		} else {
			service.deleteComment(idx);
		}

		mav.addObject("idx", linkedArticleNum); // move back to the article
		mav.setViewName("redirect:view.do");

		return mav;
	}

	@RequestMapping("/recommend.do")
	public ModelAndView updateRecommendcount(HttpServletRequest request, HttpSession session) {
		int idx = Integer.parseInt(request.getParameter("idx"));
		String userId = (String) session.getAttribute("userId");
		BoardModel board = service.getOneArticle(idx);

		ModelAndView mav = new ModelAndView();

		if (userId.equals(board.getWriterId())) {
			mav.addObject("errCode", "1");
		} else {
			service.updateRecommendCount(board.getRecommendcount() + 1, idx);
		}

		mav.addObject("idx", idx);
		mav.setViewName("redirect:/board/view.do");

		return mav;
	}
}

 

 

 

 

저 많은 코드들 중에서 boardWriteProc 메서드를 찾아줄게요.

바로 다음 부분입니다.

@RequestMapping(value = "/write.do", method = RequestMethod.POST)
	public String boardWriteProc(@ModelAttribute("BoardModel") BoardModel boardModel, MultipartHttpServletRequest request, HttpSession session) {
		String uploadPath = session.getServletContext().getRealPath("/") + "files/";
		File dir = new File(uploadPath);
		if (!dir.exists()) {
			dir.mkdir();
		}

		MultipartFile file = request.getFile("file");
		if (file != null && !"".equals(file.getOriginalFilename())) {
			String fileName = file.getOriginalFilename();
			File uploadFile = new File(uploadPath + fileName);
			if (uploadFile.exists()) {
				fileName = new Date().getTime() + fileName;
				uploadFile = new File(uploadPath + fileName);
			}

			try {
				file.transferTo(uploadFile);
			} catch (Exception e) {
				System.out.println("upload error");
			}
						
			boardModel.setFileName(fileName);
		}

		String content = boardModel.getContent().replaceAll("\r\n", "<br />");
		boardModel.setContent(content);
		service.writeArticle(boardModel);

		return "redirect:list.do";
	}

 

이제 주석을 달아서 각각 의미하는 부분들을 살펴보겠습니다.

        //업로드 파일을 저장할 경로 설정 -> WebDocumentRoot/files 디렉터리 아래에 파일 저장
		String uploadPath = session.getServletContext().getRealPath("/") + "files/";
		File dir = new File(uploadPath);
		if (!dir.exists()) {
			dir.mkdir();
		}
		
		//파일 데이터를 추출
		MultipartFile file = request.getFile("file");
		if (file != null && !"".equals(file.getOriginalFilename())) {
			//파일명을 추출
			String fileName = file.getOriginalFilename();
			//업로드 파일을 저장하는 데 업로드 파일명과 동일한 파일명을 사용
			File uploadFile = new File(uploadPath + fileName);
			if (uploadFile.exists()) {
				fileName = new Date().getTime() + fileName;
				uploadFile = new File(uploadPath + fileName);
			}
			
			//외부에서 접근 가능한 경로에 업로드 파일명과 동일한 파일명으로 저장
			try {
				file.transferTo(uploadFile);
			} catch (Exception e) {
				System.out.println("upload error");
			}
			
			//파일명 정보를 DB에 저장하기 위해 설정
			boardModel.setFileName(fileName);
		}

 

 

 

 

 

2. 취약점 제거

 

필요없는 부분들은 지우지 않고 주석처리했으며 번호를 달아서 취약점들을 설명했습니다.

	@RequestMapping(value = "/write.do", method = RequestMethod.POST)
	public String boardWriteProc(@ModelAttribute("BoardModel") BoardModel boardModel, MultipartHttpServletRequest request, HttpSession session) {
				
		//1.업로드 파일을 저장할 경로 설정 -> WebDocumentRoot/files 디렉터리 아래에 파일 저장
		//String uploadPath = session.getServletContext().getRealPath("/") + "files/";
		String uploadPath = "C:/SecureCoding/upload/files/";
		File dir = new File(uploadPath);
		if (!dir.exists()) {
			dir.mkdir();
		}
		
		//파일 데이터를 추출
		MultipartFile file = request.getFile("file");
		
		//2.파일 크기 제한
		if (file != null && file.getSize() > 2000000) {
			return "redirect:list.do";
		}
		
		
		if (file != null && !"".equals(file.getOriginalFilename())) {
			//파일명을 추출
			String fileName = file.getOriginalFilename();
			
			//3.파일의 확장자를 검증 (gif, jpeg로 제한)
			if (!fileName.toLowerCase().endsWith("gif") && !fileName.toLowerCase().endsWith("jpeg")) {
				return "Redirect:list.do";
			}
			
			//4.저장 파일명을 외부에서 알 수 없는 형태로 사용 (난수를 생성해서 사용)
			String savedFileName = UUID.randomUUID().toString();
						
			//File uploadFile = new File(uploadPath + fileName);
			File uploadFile = new File(uploadPath + savedFileName);
			
			//유니크한 파일이 생성되므로 아래 로직은 필요없어 주석 처리
//			if (uploadFile.exists()) {
//				fileName = new Date().getTime() + fileName;
//				uploadFile = new File(uploadPath + fileName);
//			}
			
			//외부에서 접근 가능한 경로에 업로드 파일명과 동일한 파일명으로 저장
			try {
				file.transferTo(uploadFile);
			} catch (Exception e) {
				System.out.println("upload error");
			}
			
			//5.업로드 파일 저장에 사용한 파일명을 DB에 저장할 수 있도록 설정
			//boardModel.setFileName(fileName);
			boardModel.setFileName(fileName);
		}

		String content = boardModel.getContent().replaceAll("\r\n", "<br />");
		boardModel.setContent(content);
		service.writeArticle(boardModel);

		return "redirect:list.do";
	}

 

그리고 새로운 디렉터리도 생성해주었습니다.

 

 

 

 

 

3. 다시 웹 쉘 업로드

 

확장자 검증의 로직에 걸려서 게시판 내용이 등록지 않는 것을 확인합니다.

 

 

 

 

4. 이미지 파일 업로드

 

 

 

 

 

 

 

5. 업로드 내용 확인

 

참고로 코드에는 jpeg라고 해주어서 사진업로드가 안되는 문제가 발생했습니다.

그래서 jpeg로 확장자를 고쳐서 다시 재업로드했습니다.

 

이미지가 안나오고 있죠??

접근할 수 없는 경로에 들어가 있다는 것입니다.

 

 

 

 

 

 

6. 개발 도구를 이용해서 이미지 출력 부분 확인

 

 

웹 루트 아래 디렉터리를 지정하고 있음에도 업로드 파일의 저장 경로와 일치하지 않겠죠??

 

 

 

 

 

 

 

7. @WinXP 가상머신에서 업로드 파일 확인

 

 

 

눌러서 확인해보니 다음과 같이 업로드했던 파일과 동일합니다!!

 

 

 

 

8. 파일 다운로드 기능 구현

 

 

@WinXP 가상머신에서 Eclipse - BoardController.java 파일로 가줄게요

 

다음과 같이 새로운 코드 추가

//http://winxp:8080/openeg/get_image.do?filename=원본파일명
	//요청이 들어왔을 때 동작하는 메서드를 정의
	@RequestMapping("/get_image.do")
	public void getImage(HttpServletRequest request, HttpServletResponse response, HttpSession session) {
		
		//서버 세션에 저장되어 있는 게시판 정보(원본 파일명과 저장된 파일명)를 추출
		BoardModel board = (BoardModel)session.getAttribute("board");
		
		//요청 파라미터로 전달된 원본 파일명을 추출
		String filename = request.getParameter("filename");
		
		//세션에 저장된 원본 파일명과 요청 파라미터로 전달된 원본 파일명을 비교
		//접근할 수 없는 파일에 대해 접근하는 것을 방지하기 위해
		if (filename != null && filename.equals(board.getFileName())) {
			//세션에 저장된 저장 파일명을 추출
			String savedFileName = board.getSavedFileName();
			//파일 저장 경로를 생성
			String filePath = "C:/SecureCoding/upload/files/" + savedFileName;
			
			//파일을 읽어서 응답으로 전송
			BufferedOutputStream out = null;
			InputStream in = null;
			try {
				response.setContentType("image/jpeg");
				response.setHeader("Content-Disposition", "inline; filename=" + filename);
				
				File file = new File(filePath);
				in = new FileInputStream(file);
				out = new BufferedOutputStream(response.getOutputStream());
				
				int len;
				byte[] buff = new byte[1024];
				while ((len = in.read(buff)) > 0) {
					out.write(buff, 0, len);
				}
			} catch(Exception e) {
				System.out.println("파일 전송 오류");
			} finally {
				if (out != null) {
					try { out.close(); } catch(Exception e) {}
				}
				if (in != null) {
					try { in.close(); } catch(Exception e) {}
				}
			}
		}
	}

 

그리고 기존의 boardView 메서드 부분을 다음과 같이 고치겠습니다.

//게시판 상세 페이지 조회 처리
	//이미지 파일 다운로드 사용하기 위해 게시판 정보를 세션에 저장
	@RequestMapping("/view.do")
	public ModelAndView boardView(HttpServletRequest request, HttpSession session) {
		int idx = Integer.parseInt(request.getParameter("idx"));
		
		//게시판 정보가 board 변수에 저장되어 있음
		BoardModel board = service.getOneArticle(idx);
		
		//board 변수를 세션에 저장
		session.setAttribute("board", board);
		
		service.updateHitcount(board.getHitcount() + 1, idx);

		List<BoardCommentModel> commentList = service.getCommentList(idx);

		ModelAndView mav = new ModelAndView();
		mav.addObject("board", board);
		mav.addObject("commentList", commentList);
		mav.setViewName("/board/view");
		return mav;
	}

 

 

9. 게시판 상세 화면에 다운로드 기능 추가

 

 

@WinXP - Eclipse - view.jsp 찾아주기

 

view.jsp 파일의 전체 코드는 다음과 같습니다.

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>글 보기: ${board.subject}</title>
<link href="<%=request.getContextPath()%>/css/board.css"
	rel="stylesheet" type="text/css" />
<link href="<%=request.getContextPath()%>/css/main.css" rel="stylesheet"
	type="text/css" />
<script type="text/javascript">
	function errCodeCheck(){
		var errCode = <%=request.getParameter("errCode")%>;
		if(errCode != null || errCode != ""){
			switch (errCode) {
			case 1:
				alert("잘못된 접근 경로입니다!");
				break;
			case 2:
				alert("댓글이 있어 글을 삭제하실 수 없습니다!");
				break;
			}
		}		
	}
	
	function commentDelete(commentIdx, linkedArticleNum){
		if(confirm("선택하신 댓글을 삭제하시겠습니까?")){
			location.href("commentDelete.do?idx=" + commentIdx + "&linkedArticleNum=" + linkedArticleNum);
		}		
	}
	
	function moveAction(where){
		switch (where) {
		case 1:
			if(confirm("글을 삭제하시겠습니까?")){
				location.href ="delete.do?idx=${board.idx}";
			}
			break;

		case 2:
			if(confirm-("글을 수정하시겠습니까?")){
				location.href = "modify.do?idx=${board.idx}";
			}
			break;
			
		case 3:
			location.href = "list.do";			
			break;
		
		case 4:
			if(confirm("글을 추천하시겠습니까?")){
				location.href = "recommend.do?idx=${board.idx}";
			}
			break;
		}
	}
</script>
</head>
<body onload="errCodeCheck()">
	<div id="container">

		<h1>
			<jsp:include page="header.jsp" />
		</h1>


		<div id="content-container">
			<div id="content">
				<table class="boardView">
					<tr>
						<td colspan="4"><h3>${board.subject}</h3></td>
					</tr>
					<tr>
						<th>작성자</th>
						<th>조회수</th>
						<th>추천수</th>
						<th>작성일</th>
					</tr>
					<tr>
						<td>${board.writer}</td>
						<td>${board.hitcount}</td>
						<td>${board.recommendcount}</td>
						<td>${board.writeDate}</td>
					</tr>
					<tr>
						<th colspan="4">내용</th>
					</tr>
					<c:if test="${board.fileName != null}">
						<tr>
							<td colspan="4" align="left">
								첨부파일 : 
								<br /><a href="../files/${board.fileName}" target="_blank">${board.fileName}</a>
								<br /><img src="../files/${board.fileName}" />
							</td>
						</tr>
					</c:if>
					<tr>
						<td colspan="4" align="left">
							${board.content}
						</td>
					</tr>
				</table>
				<table class="commentView">
					<tr>
						<th colspan="2">댓글</th>
					</tr>
					<c:forEach var="comment" items="${commentList}">
						<tr>
							<td class="writer">
								<p>${comment.writer}
									<c:if test="${comment.writerId == userId}">
										<br />
										<a onclick="commentDelete(${comment.idx}, ${board.idx})"><small>댓글
												삭제</small></a>
									</c:if>
								</p>
							</td>
							<td class="content" align="left"><span class="date">${comment.writeDate}</span>
								<p>${comment.content}</p></td>
						</tr>
					</c:forEach>
					<tr>
						<td class="writer2"><strong>댓글 쓰기</strong></td>
						<td class="content2">
							<form action="commentWrite.do" method="post">
								<input type="hidden" id="writer" name="writer"
									value="${userName}" /> <input type="hidden" id="writerId"
									name="writerId" value="${userId}" /> <input type="hidden"
									id="linkedArticleNum" name="linkedArticleNum"
									value="${board.idx}" />
								<textarea id="content" name="content" class="commentForm1"></textarea>
								<input type="submit" value="확인" class="commentBt" />
							</form>
						</td>
					</tr>
				</table>
				<br />
				<center>
					<c:choose>
						<c:when test="${board.writerId == userId}">
							<input type="button" value="삭제" class="writeBt"
								onclick="moveAction(1)" />
							<input type="button" value="수정" class="writeBt"
								onclick="moveAction(2)" />
							<input type="button" value="목록" class="writeBt"
								onclick="moveAction(3)" />
						</c:when>
						<c:otherwise>
							<input type="button" value="추천" class="writeBt"
								onclick="moveAction(4)" />
							<input type="button" value="목록" class="writeBt"
								onclick="moveAction(3)" />
						</c:otherwise>
					</c:choose>
				</center>
			</div>
			<div id="aside">
				<fieldset>
					<center>
						<label>[ ${userName} ]님 환영합니다.</label><br /> <a
							href="../logout.do">로그아웃</a>&nbsp;&nbsp;&nbsp; <a
							href="../member/modify.do">정보수정</a>
					</center>
				</fieldset>
			</div>
			<div id="footer">
				<jsp:include page="footer.jsp" />
			</div>





		</div>
	</div>
</body>
</html>

 

다음 코드 부분을 봐주세요:)

<td colspan="4" align="left">
								첨부파일 : 
								<br /><a href="../files/${board.fileName}" target="_blank">${board.fileName}</a>
								<br /><img src="../files/${board.fileName}" />
							</td>

 

 

다음과 같이 조금 수정해 줄게요:)

	<td colspan="4" align="left">
								첨부파일 : 
								<p>변경 전 코드 : 웹 루트 아래에 있는 파일을 직접 액세스</p>
								<br /><a href="../files/${board.fileName}" target="_blank">${board.fileName}</a>
								<br /><img src="../files/${board.fileName}" />
								
								<p>변경 후 코드 : 웹 루트 밖에 있는 파일을 다운로드 요청</p>
								<br /><a href="get_image.do?filename=${board.fileName}" target="_blank">${board.fileName}</a>
								<br /><img src="get_image.do?filename=${board.fileName}" />
							</td>

 

 

 

 

10. 앞에서 업로드한 이미지가 포함된 상세 페이지 조회

 

하하핳하하핳 또 안나와서 대환장 파티!!!

코드가 틀렸는 지.... 아무리 봐도 못찾겠네요ㅠㅠ

 

 

728x90
반응형