저번 시간에는 Pagination 기능을 구현하였습니다.
https://jamesbexter.tistory.com/entry/웹-개발-PHP-Pagination-기능-추가-보완할-점
[웹 개발-PHP] Pagination 기능 추가(+ 보완할 점)
저번시간엔 마이페이지에서 개인정보 수정 및 비밀번호 변경 기능을 구현했었습니다. https://jamesbexter.tistory.com/entry/%EC%9B%B9-%EA%B0%9C%EB%B0%9C-%EB%A7%88%EC%9D%B4%ED%8E%98%EC%9D%B4%EC%A7%80%EB%B9%84%EB%B0%80%EB%B2%88%
jamesbexter.tistory.com
"이번시간에는 파일 업로드 기능을 구현해보겠습니다!"
마침 최근에 파일 업로드 취약점을 공부하고 있어서 공격에 대한 이해를 위하여 직접 구현해보았습니다!
< 구현하기 전에 구상해보기 >
1. 일단 파일을 저장해놓을 장소가 필요하다.
2. 파일을 첨부하는 기능이 있는곳은 CRUD중 C(create) , U(update) 에 기능을 넣어야 할 것같다.
3. 링크를 직접적으로 보여주기 보단 보기좋게 버튼을 클릭하여 다운로드 받을수 있게 구현해보자.
4. 게시글을 삭제를 할 시엔 서버에 파일도 같이 삭제해야 한다.
5. (+ 확장자 체크)
< 1. 파일을 저장해놓을 장소가 필요하다. >
이를 위하여 로그인 시 data 라는 폴더에 SESSION 에 저장된 닉네임을 제목으로 하는 폴더가 만들어지게 하였습니다.
만약 자신의 닉네임과 같은 폴더가 이미 있을시 스킵되도록 처리 하였습니다.
로그인 인증 통과시 자동으로 폴더가 만들어집니다.
[로그인 인증 & 폴더 생성 코드]
if($row['password']==$password_hash){ //로그인 성공시
$_SESSION['userId'] = $user_id;
$_SESSION['username'] = $row['name'];
$_SESSION['loggedin'] = true;
$dirname = "./data/".$_SESSION['username']; // 파일첨부용 개인폴더 생성(이미 있을시엔 생성x)
if (!file_exists($dirname)) {
$dirmake = mkdir($dirname, 0777);
}
header('Location: /page.php'); //리다이렉션 하는 코드
exit();
}
< 2. Form 태그와 Input 태그를 이용하여 파일을 업로드를 할 수 있을것 같다>
게시글 작성 및 수정 HTML 코드 중, FORM 태그 부분에 entype="multipart/form-data" 를 추가하여 파일을 업로드 할 수 있도록 만들어 주었습니다.
[글쓰기 페이지 Form Tag]
<form action="post_write_prc.php" method="POST" enctype="multipart/form-data">
<input type="text" name="post_name" placeholder="제목" maxlength='20' style="text-align:center;position:absolute;background-color:white; width:60%; height:10%;transform:translate(33%,50%);font-size:20px" class="login-box">
<textarea type="text" name="contents" placeholder="내용" maxlength='750' style="text-align:center;position:absolute;background-color:white; width:60%; height:65%;transform:translate(33%,25%)" class="login-box"></textarea>
<input type="file" name="myfile" style="position:absolute;right:43%;bottom:70px;transform:translate(33%,25%)">
<div style="position:absolute;bottom:20px;right:39.5%;width:20%;">
<input style="height:30px;background-color:#dcdcdc" type = submit class="submit-btn" value="작성"></div>
</div>
[게시글 수정 페이지 Form Tag]
form action="post_update_prc.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="idx" value=<?php echo $idx; ?>>
<input type="text" name="post_name" value="<?php echo $post_name ?>" placeholder="제목" maxlength='20' style="text-align:center;position:absolute;background-color:white; width:60%; height:10%;transform:translate(33%,50%);font-size:20px" class="login-box">
<textarea type="text" name="contents" placeholder="내용" maxlength='750' style="text-align:center;position:absolute;background-color:white; width:60%; height:65%;transform:translate(33%,25%)" class="login-box"><?php echo $contents; ?></textarea>
<input type="file" name="myfile" style="position:absolute;right:43%;bottom:70px;transform:translate(33%,25%)">
<div style="position:absolute;bottom:20px;right:39.5%;width:20%;">
<input style="height:30px;background-color:#dcdcdc" type = submit class="submit-btn" value="작성"></div>
</form>
이 Form 데이터가 POST 로 각각 post_write_prc.php , post_update_prc.php 로 전송이 됩니다..
이후 post_write_prc.php 에선 전송된 파일을 업로드 하고 , SQL 서버에 파일이름을 저장하게 됩니다.
또한 post_update_prc.php 에선 전송된 파일을 업로드 하고 , 기존에 있던 파일을 삭제하는 동작을 합니다.
파일 업로드를 위해서 move_uploaded_file() 함수를 썻고,기존파일 삭제를 위해 unlink() 함수를 사용했습니다.
[post_write_prc.php 코드]
<?php
include 'dbcon.php';
session_start();
if(!($_SESSION['loggedin'])){ // 점프해서 mypage.php 로 오는것을 방지.
header('Location: /index.php');
exit();}
$post_name=$_POST['post_name'];
$contents=$_POST['contents'];
$post_writer=$_SESSION['username'];
$file=$_FILES['myfile']['name']; //SQL용 파일이름 저장
$uploaded_file_name_tmp = $_FILES['myfile']['tmp_name'];
$uploaded_file_name=$_FILES['myfile']['name']; //파일옮기기용 파일이름 저장
$upload_folder="./data/".$_SESSION['username']."/";
move_uploaded_file( $uploaded_file_name_tmp, $upload_folder . $uploaded_file_name );
if($post_name==NULL||$contents==NULL){
echo "<script>alert('작성하지 않은 내용이 있습니다!');
window.location.href='/'
</script>";
}else{
$sql = "insert into postinfo (post_name,contents,postwriter,FILE) values ('".$post_name."','".$contents."','".$post_writer."','".$file."')";
$result = mysqli_query($dbcon,$sql);
$sql="UPDATE postinfo set postdate=CONVERT_TZ(NOW(), '+00:00', '+09:00') where post_name='".$post_name."'";
$result = mysqli_query($dbcon,$sql); //서버 시간대가 UTC 라서 9시간을 더하기.
echo "<script>alert('게시글이 작성되었습니다!!');
location.href='/index.php'
</script>";
exit();
}
?>
[post_update_prc.php 코드]
<?php
include 'dbcon.php';
session_start();
if(!($_SESSION['loggedin'])){ // 점프해서 page.php 로 오는것을 방지.
header('Location: /index.php');
exit();
}
$idx=$_POST['idx']; // DB에서 update 를 위한 인증정보 가져오기
$sql = "select * from postinfo where idx=".$idx;
$result = mysqli_query($dbcon,$sql);
$row=mysqli_fetch_array($result);
$post_writer=$row['postwriter']; // 수정할 정보 변수설정
$post_name=$_POST['post_name'];
$contents=$_POST['contents'];
$file=$_FILES['myfile']['name']; //SQL용 파일이름 저장
$uploaded_file_name_tmp = $_FILES['myfile']['tmp_name'];//임시파일경로
$uploaded_file_name=$_FILES['myfile']['name']; //파일옮기기용 파일이름 저장
$upload_folder="./data/".$_SESSION['username']."/";//업로드할 파일 경로
$remove_folder="./data/".$post_writer."/".$row['FILE']; //기존에 있던 파일 경로
if($post_writer==$_SESSION['username']){ //수정 시작전에 글작성자와 세션의 사용자가 같은지 인증
$sql = "update postinfo set post_name='".$post_name."',contents='".$contents."',FILE='".$file."' where idx=".$idx;
$result = mysqli_query($dbcon,$sql);
unlink($remove_folder); //기존에 있던 파일 삭제
move_uploaded_file( $uploaded_file_name_tmp, $upload_folder . $uploaded_file_name );//새로운 파일 업로드
//인증 완료시 게시글 삭제
echo "<script>alert('게시글이 수정되었습니다!!');
location.href='/index.php'
</script>";
exit();
}else{ //인증 X 일때 권한이 없다한 후 index.php 로 리다이렉션
echo "<script>alert('잘못된 접근입니다.');
location.href='/index.php'
</script>";
exit();
}
?>
<3.링크를 직접적으로 보여주기 보단 보기좋게 버튼을 클릭하여 다운로드 받을수 있게 구현해보자.>
페이지 링크를 클릭하여 다운로드 받는것 보단 버튼을 누르는게 좀 더 가독성이 좋아보여서 추가해주었습니다.
게시글 읽기 페이지인 post_read.php 로 들어가면 "저장한 파일이 있냐 없냐" 유무에 따라 Download 버튼이 보이고 안보이고 할 수 있게 만들어주었습니다.
[post_read.php 중 Download 버튼 코드]
<?php if($row['FILE']!=NULL){ ?>
<form action="download.php" method="POST">
<input type="hidden" name="file" value="<?php echo $row['FILE']; ?>">
<input type="hidden" name="postwriter" value="<?php echo $row['postwriter']; ?>">
<input style="position:absolute;bottom:10%;right:32%;height:30px;width:34%" type = submit class="download-btn" value="FILE Download">
<?php } ?>
</form>
이렇게 Formdata 를 보내주게 되면 download.php 에선 파일의 링크를 자동으로 클릭해주고 다시 돌아오게 됩니다.
addEventListener를 추가하여 페이지가 로드되자마자 <a> 태그를 클릭해주게 만들었습니다.
[download.php]
<?php
include 'dbcon.php';
session_start();
if(!($_SESSION['loggedin'])){ // 점프해서 mypage.php 로 오는것을 방지.
header('Location: /index.php');
exit();}
$file=$_POST['file'];
$postwriter=$_POST['postwriter'];
$sql = "select * from postinfo where FILE='".$file."'";
$result = mysqli_query($dbcon,$sql);
$row=mysqli_fetch_array($result);
?>
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<script>
document.addEventListener("DOMContentLoaded", () => {
document.getElementById('download-link').click();
});
</script>
</head>
<body>
<a style="display:none" id ="download-link" href="data/<?php echo $postwriter.'/'.$file ?>" download>파일 다운로드</a>
<?php
echo "<script> window.history.back(); </script>";
?>
</body>
</html>
< 4. 게시글을 삭제할 시엔 서버의 파일도 삭제해야한다. >
게시글이 삭제되었는데 파일이 여전히 남아있으면 보안상으로 매우 위험합니다.
그리하여 게시글 삭제시 파일도 같이 삭제되도록 만들어 주었습니다.
[post_delete.php]
<?php
include 'dbcon.php';
session_start();
if(!($_SESSION['loggedin'])){ // 점프해서 page.php 로 오는것을 방지.
header('Location: /index.php');
exit();
}
$idx=$_POST['idx']; // DB에서 delete 를 위한 인증정보 가져오기
$sql = "select * from postinfo where idx=".$idx;
$result = mysqli_query($dbcon,$sql);
$row=mysqli_fetch_array($result);
$post_writer=$row['postwriter'];
$remove_folder="./data/".$post_writer."/".$row['FILE']; //기존에 있던 파일 경로
if($post_writer==$_SESSION['username']){ //글작성자와 세션의 사용자가 같은지 인증
$sql = "delete from postinfo where idx=".$idx;
$result = mysqli_query($dbcon,$sql);
unlink($remove_folder);
//인증 완료시 게시글 삭제
echo "<script>alert('게시글이 삭제되었습니다!!');
location.href='/index.php'
</script>";
exit();
}else{ //인증 X 일때 권한이 없다한 후 index.php 로 리다이렉션
echo "<script>alert('권한이 없습니다.');
location.href='/index.php'
</script>";
exit();
}
?>
< + 확장자 체크 >
글을 적고 난 후 다시 생각해보니 확장자 체크를 안해주었던 것 같습니다.
이렇게 되면 php 파일을 서버에 업로드 하고 실행하는 파일업로드 문제가 발생할 것입니다.
그래서 post_write_prc.php 와 post_update_prc.php 에 확장자 체크 코드를 추가해주었습니다.
제가 올리는 코드의 확장자 체크과정은 3단계입니다.
1. allowed_ext 으로 미리 허용할 확장자를 적어둔다.
2. pathinfo 함수를 통해 form data 로 도착한 파일의 확장자만 추출하여 $upload_file_ext 의 적어준다.
3.in_array 함수를 통하여 파일의 확장자가 허용하는 확장자 인지 체크를 한 후 boolean 변수를 이용해 체크해준다.
[post_write_prc.php]
<?php
include 'dbcon.php';
session_start();
if(!($_SESSION['loggedin'])){ // 점프해서 mypage.php 로 오는것을 방지.
header('Location: /index.php');
exit();}
$post_name=$_POST['post_name'];
$contents=$_POST['contents'];
$post_writer=$_SESSION['username'];
$file=$_FILES['myfile']['name']; //SQL용 파일이름 저장
$uploaded_file_name_tmp = $_FILES['myfile']['tmp_name'];
$uploaded_file_name=$_FILES['myfile']['name']; //파일옮기기용 파일이름 저장
$upload_folder="./data/".$_SESSION['username']."/";
$allowed_ext=['jpg','bmp','jpeg','png','gif','txt','xls','xlsx']; //허용할 확장자
if($file!=''){
$upload_file_ext=strtolower(pathinfo($file, PATHINFO_EXTENSION)); // . 뒤에만 남기고 다 삭제
$ext_check=in_array($upload_file_ext,$allowed_ext); //파일 확장자의 대소문자 구분 없애고,허용할 확장자가 맞는지 체크
if($ext_check){ //만약 확장자가 허용된다면 파일을 서버에 업로드
move_uploaded_file( $uploaded_file_name_tmp, $upload_folder . $uploaded_file_name );
}else{ //허용되지 않는 확장자면 경고후 뒤로가기
echo "<script>alert('허용되지 않는 파일의 확장자가 검출되었습니다.');
window.location.href='/'
</script>";
exit();
}
}else{
$uploaded_file_name=''; //파일이 없을경우엔 빈문자열로 지정
}
if($post_name==NULL||$contents==NULL){
echo "<script>alert('작성하지 않은 내용이 있습니다!');
window.location.href='/'
</script>";
exit();
}else{
$sql = "insert into postinfo (post_name,contents,postwriter,FILE) values ('".$post_name."','".$contents."','".$post_writer."','".$file."')";
$result = mysqli_query($dbcon,$sql);
$sql="UPDATE postinfo set postdate=CONVERT_TZ(NOW(), '+00:00', '+09:00') where post_name='".$post_name."'";
$result = mysqli_query($dbcon,$sql); //서버 시간대가 UTC 라서 9시간을 더하기.
echo "<script>alert('게시글이 작성되었습니다!!');
location.href='/index.php'
</script>";
exit();
}
?>
[post_update_prc.php]
<?php
include 'dbcon.php';
session_start();
if(!($_SESSION['loggedin'])){ // 점프해서 page.php 로 오는것을 방지.
header('Location: /index.php');
exit();
}
$idx=$_POST['idx']; // DB에서 update 를 위한 인증정보 가져오기
$sql = "select * from postinfo where idx=".$idx;
$result = mysqli_query($dbcon,$sql);
$row=mysqli_fetch_array($result);
$post_writer=$row['postwriter']; // 수정할 정보 변수설정
$post_name=$_POST['post_name'];
$contents=$_POST['contents'];
$file=$_FILES['myfile']['name']; //SQL용 파일이름 저장
$uploaded_file_name_tmp = $_FILES['myfile']['tmp_name'];//임시파일경로
$uploaded_file_name=$_FILES['myfile']['name']; //파일옮기기용 파일이름 저장
$upload_folder="./data/".$_SESSION['username']."/";//업로드할 파일 경로
$remove_folder="./data/".$post_writer."/".$row['FILE']; //기존에 있던 파일 경로
$allowed_ext=['jpg','bmp','jpeg','png','gif','txt','xls','xlsx']; //허용할 확장자
if($file!=''){
$upload_file_ext=strtolower(pathinfo($file, PATHINFO_EXTENSION)); // . 뒤에만 남기고 다 삭제
$ext_check=in_array($upload_file_ext,$allowed_ext); //파일 확장자의 대소문자 구분 없애고,허용할 확장자가 맞는지 체크
if($ext_check){ //만약 확장자가 허용된다면 이후 코드 진행
}else{ //허용되지 않는 확장자면 경고후 뒤로가기
echo "<script>alert('허용되지 않는 파일의 확장자가 검출되었습니다.');
window.location.href='/'
</script>";
exit();
}
}else{
$uploaded_file_name=''; //파일이 없을경우엔 빈문자열로 지정
}
if($post_writer==$_SESSION['username']){ //수정 시작전에 글작성자와 세션의 사용자가 같은지 인증
$sql = "update postinfo set post_name='".$post_name."',contents='".$contents."',FILE='".$file."' where idx=".$idx;
$result = mysqli_query($dbcon,$sql);
unlink($remove_folder); //기존에 있던 파일 삭제
move_uploaded_file( $uploaded_file_name_tmp, $upload_folder . $uploaded_file_name );//새로운 파일 업로드
//인증 완료시 게시글 삭제
echo "<script>alert('게시글이 수정되었습니다!!');
location.href='/index.php'
</script>";
exit();
}else{ //인증 X 일때 권한이 없다한 후 index.php 로 리다이렉션
echo "<script>alert('잘못된 접근입니다.');
location.href='/index.php'
</script>";
exit();
}
?>
[결과]
게시글 작성 /수정 페이지는 똑같이 생겨서 한번에 첨부했습니다!..
post_write.php / post_update.php |
![]() |
설정해놓았던 경로를 따라 저장이 된것을 볼 수 있습니다.
서버 컴퓨터 로컬 파일 |
![]() |
파일 업로드 유무에 따라 Download 버튼의 생성유무가 다르다는 것을 볼 수 있습니다.
FILE Download클릭시 저장되어 있던 파일이 다운로드가 됩니다.
파일이 업로드 된 게시물 | 파일이 업로드 되지 않은 게시물 |
![]() |
![]() |
긴 글 읽어주셔서 감사합니다!
'웹개발(PHP-Mysql)' 카테고리의 다른 글
[웹 개발-Jquery] 비밀번호 가시화 기능 (0) | 2024.07.21 |
---|---|
[웹 개발-PHP] 조회수 & 좋아요 기능 구현 (2) | 2024.07.21 |
[웹 개발-PHP] Pagination 기능 추가(+ 보완할 점) (0) | 2024.07.17 |
[웹 개발] 마이페이지(비밀번호 변경 & 개인정보 수정) (0) | 2024.07.16 |
[웹 개발] 검색 및 정렬 기능 구현 (0) | 2024.07.16 |