[웹 개발-PHP] 파일 업로드 기능 구현(+확장자 체크)

2024. 7. 20. 02:59·웹개발(PHP-Mysql)

 

저번 시간에는 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
'웹개발(PHP-Mysql)' 카테고리의 다른 글
  • [웹 개발-Jquery] 비밀번호 가시화 기능
  • [웹 개발-PHP] 조회수 & 좋아요 기능 구현
  • [웹 개발-PHP] Pagination 기능 추가(+ 보완할 점)
  • [웹 개발] 마이페이지(비밀번호 변경 & 개인정보 수정)
무너박사
무너박사
IT 보안 블로그 입니다. 제가 작성하는 블로그가 누군가의 공부에 조금이라도 도움이 되길 바라며 작성하였습니다.
  • 무너박사
    무너박사의 연구일지
    무너박사
  • 전체
    오늘
    어제
    • 분류 전체보기 (104)
      • WEB 지식 (3)
      • 웹해킹 (13)
      • 웹개발(PHP-Mysql) (12)
      • 웹개발(JSP-Oracle) (2)
      • 워게임 문제풀이 (19)
        • Segfault (17)
        • Dreamhack (2)
      • SQL (3)
      • Python (2)
      • AI (1)
        • LLM(Large Language Model) (1)
      • Kail Linux (3)
      • 잡다한 지식 (2)
      • 모바일 앱개발(Kotlin-PHP-Mysql) (13)
      • 모바일 앱해킹(Android) (31)
        • Frida Lab (2)
        • Android DIVA (8)
        • Insecure Bank (20)
      • 안드로이드 위협 탐지 및 우회 (0)
        • 루팅 탐지 & 우회 (0)
        • 디버깅 탐지 & 우회 (0)
        • 에뮬레이터 탐지 & 우회 (0)
        • Frida 탐지 & 우회 (0)
  • 블로그 메뉴

    • 링크

    • 공지사항

    • 인기 글

    • 태그

      시스템해킹
      mobile diva
      insecure bank
      앱해킹
      리패키징
      취업반
      취업반 6기
      안드로이드 스튜디오
      모의해킹
      Android Studio
      해킹
      워게임
      Kotlin
      모바일 앱개발
      Koltin
      칼리리눅스
      XSS
      인시큐어 뱅크
      MySQL
      인시큐어뱅크
      웹해킹
      normaltic
      php
      android diva
      Blind sql injection
      dom based xss
      취업반6기
      모바일 앱해킹
      sql injection
      모바일앱개발
    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.3
    무너박사
    [웹 개발-PHP] 파일 업로드 기능 구현(+확장자 체크)
    상단으로

    티스토리툴바