본문 바로가기

개발과제/보안추가

파일 업로드 다운로드 코드(DB에 저장하기)

안녕하세요 제가 만든 게시판에는 제 서버에 직접 파일을 넣고

그리고 그 파일을 다운로드하게 했잖아요?

 

그러면 취약점이 될 수 있습니다.

아무리 화이트 리스트나 블랙리스트를 작성해도 나중에 가서 우회하는 방법이 나올 수 있죠.

 

그런데 그런 파일들을 DB에 저장하게 하면 걱정이 없죠 악성코드든 어느 파일이든

DB에서는 실행이 불가능하니깐요!

 

그러니 보안 코딩을 하겠습니다.

 

우선적으로 저희는 CLOB랑 BLOB을 알아야 합니다.

 

- CLOB(Character Large Object)

문자형 대용량 객체 고정길이와 가변 길이 문자집합 지원

 

이라고 하는데 쉽게 말씀드리자면 엄청 긴 문자열 데이터를 저장하기 위해 사용됩니다.

그래서 텍스트 문서, XML 데이터, 소스 코드 등 텍스트 기반 데이터를 저장하는 데 적합하다고 하네요!

 

 

 

- BLOB(Binary Large Object)

이진 데이터를 저장하기 위해 사용할 때 이용합니다.

그러니 이미지, 오디오, 비디오, 문서 파일 같은 이진 형식의 데이터를 저장하는 데 적합합니다!

 

 

 

저희는 이 BLOB 형식을 채용해서 DB에 저장을 하겠습니다.

 

BLOB 형식으로 저장을 하기 위해 phpmyadmin에

 

이렇게

 

BOARD_INFO 에 새로운 칼럼을 추가했습니다.

 

그리고 DB에 올릴 때 b로 올리라는데.. 안돼서 s로 올렸더니 되네요!

 

주요 코드는

 

//파일 데이터 읽기
$file_data = file_get_contents($file_tmp_name);

$sql_file = "UPDATE $db_board SET file_upload = ? , file_name= ? WHERE board_id = ?";
$stmt_file = mysqli_prepare($conn, $sql_file);
mysqli_stmt_bind_param($stmt_file, 'ssi', $file_data, $new_file_name ,$board_id);

입니다.

 

총 코드 보여드릴게요

 

 

 

-write_process_board.php

<?php
    include 'DB_INFO.php'; //데이터 베이스 정보

    session_start(); //세션 시작

    if(!isset($_SESSION['login_id'])) {
        //로그인하지 않은 사용자
        header("Location: login.php"); //login 화면으로 바꾼다
        exit(); //이 페이지를 바로 닫는다
    }

    //게시판 데이터베이스 연결
    $conn = mysqli_connect($host,$username,$password, $db_board);

    //데이터베이스 오류시 종료
    if(mysqli_connect_errno()) {
        die("데이터 베이스 오류: ". mysqli_connect_error());
    }

    //POST로 전달된 정보 받기
    if(isset($_POST['board_id'])){
        $board_id = filter_var(strip_tags($_POST['board_id']),FILTER_SANITIZE_SPECIAL_CHARS);
    } else {
        $board_id = null;
    }
    $title = filter_var(strip_tags($_POST['title']),FILTER_SANITIZE_SPECIAL_CHARS);
    $detail = filter_var(strip_tags($_POST['detail']),FILTER_SANITIZE_SPECIAL_CHARS);

    //session으로 유저 이름 받기
    $user_id = $_SESSION['login_id'];

    if(isset($board_id)) {
        //board_id가 있다는 것은 수정을 의미
        $sql = "UPDATE $db_board SET title = ?, detail= ? WHERE board_id= ? ";
        //preparedstatement 적용
        $stmt = mysqli_prepare($conn, $sql);
        mysqli_stmt_bind_param($stmt, 'ssi', $title, $detail, $board_id);
    }
    else {
        //board_id가 없으니 새로 만드는 sql문
        $sql = "INSERT INTO $db_board (id, title, detail) VALUES (?, ?, ?)";
        $stmt = mysqli_prepare($conn, $sql);
        mysqli_stmt_bind_param($stmt, 'sss', $user_id ,$title, $detail);
    }

    //sql문 실행
    if(mysqli_stmt_execute($stmt)) {
        
        if(isset($board_id)) {
            $_SESSION['write_error'] = '수정되었습니다';
        }
        else {
            $board_id = mysqli_insert_id($conn);
            $_SESSION['write_error'] = '작성되었습니다';
        }
    } else {
        $_SESSION['write_error'] = '작성 중 오류가 발생하였습니다.';
        header("Location: view_board.php");
    }

    if ($_SERVER['REQUEST_METHOD'] == 'POST' && !empty($_FILES['file'])) {
        // 업로드된 파일 정보 가져오기
        $file_name = $_FILES['file']['name'];
        $timestamp = time(); // 현재 시간을 초로 반환
        $new_file_name = $timestamp . '_' .$file_name; // 현재 시간과 원래 파일 이름을 합쳐 새로운 파일 이름 생성
        $file_tmp_name = $_FILES['file']['tmp_name'];
        $file_size = $_FILES['file']['size'];
        $file_error = $_FILES['file']['error'];
        $allowed_mime_types = ['image/jpeg', 'image/png', 'image/gif','text/plain','application/zip','application/x-hwp','application/msword','application/vnd.ms-excel','application/pdf']; //MIME 허락 된 것
        //확장자 허락 된 것
        $allowed_extensions = array("jpg","png","gif","txt","zip","hwp","word","xls","xlsx","pdf");

        //sql공격 방지용
        $new_file_name = mysqli_real_escape_string($conn,$new_file_name);

        //파일 업로드가 정상적으로 처리되었는지 확인
        if ($file_error === UPLOAD_ERR_OK) {
            //파일 MIME 타입 확인, 조작됬는지 확인
            $file_mime_type = mime_content_type($file_tmp_name);
            $finfo = finfo_open(FILEINFO_MIME_TYPE);
            $file_mime_type = finfo_file($finfo, $file_tmp_name);
            finfo_close($finfo);
            
            if (in_array($file_mime_type, $allowed_mime_types)) {

                //파일 확장자 확인
                $file_ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
                if(in_array($file_ext, $allowed_extensions)) {

                    //파일 데이터 읽기
                    $file_data = file_get_contents($file_tmp_name);

                    $sql_file = "UPDATE $db_board SET file_upload = ? , file_name= ? WHERE board_id = ?";
                    $stmt_file = mysqli_prepare($conn, $sql_file);
                    mysqli_stmt_bind_param($stmt_file, 'ssi', $file_data, $new_file_name ,$board_id);

                    // 파일 이동 및 저장
                    if (mysqli_stmt_execute($stmt_file)) {
                        $_SESSION['file_error'] =  '파일 업로드 성공';
                    } else {
                        $_SESSION['file_error'] =  '파일 업로드 실패';
                    }
                } else {
                    $_SESSION['file_error'] =  '잘못된 파일 형식입니다';
                }
            } else {
                $_SESSION['file_error'] =  '잘못된 파일 형식입니다';
            }
        }
    }

    header("Location: view_board.php?index=$board_id");

    mysqli_stmt_close($stmt);
    if (isset($stmt_file)) {
        mysqli_stmt_close($stmt_file);
    }

    exit();
?>

 

 

 

 

-view_board.php

<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <link rel="stylesheet" href = "/css/style_view.css">
        <title>게시물 보는중</title>
    </head>
    <body>
        <h1>VIEWING</h1>
        <p>
        <?php 

            include 'DB_INFO.php'; //데이터 베이스 정보

            //데이터베이스 연결
            $conn = mysqli_connect($host,$username,$password,$db_board);

            //데이터베이스 오류시 종료
            if(mysqli_connect_errno()) {
                die("데이터 베이스 오류: ". mysqli_connect_error());
            }

            session_start(); //세션 시작

            if(!isset($_SESSION['login_id'])) {
                //로그인하지 않은 사용자
                header("Location: login.php"); //login 화면으로 바꾼다
                exit(); //이 페이지를 바로 닫는다
            }

            //GET방식으로 전달된 index 받는다
            $board_id = filter_var(strip_tags($_GET['index']),FILTER_SANITIZE_SPECIAL_CHARS);
            //가독성 위해 다른 변수에 저장
            $last_viewed_time_for_board_id = 'last_view_time_'.$board_id;

            // 세션에 마지막 조회 시간 정보가 있는지 확인
            if (!isset($_SESSION[$last_viewed_time_for_board_id])) {
                // 세션에 마지막 조회 시간 정보가 없으면 현재 시간을 저장
                $_SESSION[$last_viewed_time_for_board_id] = time();
            } else {
                // 세션에 마지막 조회 시간 정보가 있으면 일정 시간이 지났는지 확인
                $last_view_time = $_SESSION[$last_viewed_time_for_board_id];
                $current_time = time();
                $time_diff = $current_time - $last_view_time;
                if ($time_diff >= 60) { // 60초(1시간) 이상 지났으면 조회수 증가
                    //조회수 증가하는 sql문
                    $sql_view = "UPDATE $db_board SET views = views + 1 WHERE board_id = ? ";

                    //prepared 적용
                    $stmt = mysqli_prepare($conn, $sql_view);
                    //바인딩
                    mysqli_stmt_bind_param($stmt, 'i' , $board_id);
                    //실행
                    mysqli_stmt_execute($stmt);

                    // 세션에 현재 조회 시간을 저장
                    $_SESSION[$last_viewed_time_for_board_id] = $current_time;
                }
            }

            //작성된 게시물들 조회 문
            $sql = "SELECT * FROM $db_board WHERE board_id = ? ";
            //prepared 적용
            $stmt = mysqli_prepare($conn, $sql);
            //바인딩
            mysqli_stmt_bind_param($stmt, 'i' , $board_id);
            //실행
            mysqli_stmt_execute($stmt);
            //결과값 받기
            $result = mysqli_stmt_get_result($stmt);
            
            //게시물 출력
            while($row = mysqli_fetch_assoc($result)) {

            ?>
            <div class="view-top">
            <?php
                    echo '
                        <div class="view-top-title">'.$row['title'].'</div>

                        <div class="view-top2">
                            <div>ID: '.$row['id'].'</div>
                            
                            <div> 조회수: '.$row['views'].'</div>
                            
                            <div> 좋아요: '.$row['like_value'].'</div>
                            
                            <div> Date: '.$row['date_value'].'</div>              
                        </div>
                        <hr>
                        <div class="view-mid"> '.$row['detail'].' </div>
                        <hr>
                    ';

                

                if (isset($row['file_name'])) {
                    $file_name = implode('_', array_slice(explode('_', $row['file_name']), 1)); // '_' 문자를 기준으로 분리 후 첫 번째 요소를 제외한 나머지 요소를 모두 합쳐서 파일명을 구성합니다.
                    $download_url = 'download_process.php?filename='.$row['file_name'].'&idx='.$row['board_id'];
                    echo '
                        <div class="view-bottom"> FILE: <a href="'.$download_url.'">'.$file_name.'</a> </div>
                    
                    ';
                }
            ?>
                
                
                <div class="view-btn">
                    <!--게시물 좋아요 버튼을 보여준다-->
                    <form method='POST' action='like.php'>
                        <input type='hidden' name='board_id' value=' <?php echo $row['board_id'] ?> '>
                        <button>좋아요!</button>
                    </form>

                    <form action="write_board.php" method="POST"> 
                        <button name="write">게시판 작성</button>
                    </form>

                    <form action="board.php">
                        <button>메인페이지로</button>
                    </form>

            
            <?php


                //로그인한 사용자의 정보 가져온다
                $user_id = $_SESSION['login_id'];

                //게시물 작성자와 로그인한 ID와 일치한 경우
                if($row['id'] == $user_id) {
                    //게시물 수정 버튼을 보여준다
                    echo "<form method='GET' action='fix_board.php'>
                            <input type='hidden' name='board_id' value='".$row['board_id']."'>
                            <button>게시물 수정</button>
                          </form>";

                    //게시물 삭제 버튼을 보여준다
                    echo "<form method='POST' action='delete_board.php'>
                            <input type='hidden' name='board_id' value='".$row['board_id']."'>
                            <button>삭제</button>
                          </form>";
                }
                            
            }


            mysqli_stmt_close($stmt);
        ?>
                
            </div><!--view-btn의 div 종료점 입니다.-->

        </p>
        <p>
        <?php session_start();
                if (isset($_SESSION['write_error'])) {
                    echo $_SESSION['write_error'];
                    unset($_SESSION['write_error']);
                }
        ?>
        </p>
        <p>
        <?php session_start();
                if (isset($_SESSION['file_error'])) {
                    echo $_SESSION['file_error'];
                    unset($_SESSION['file_error']);
                }
        ?>

        </div> <!--view-top의 div 종료점 입니다.-->
    </body>
</html>

 

 

 

-download_process.php

<?php
    include 'DB_INFO.php'; //데이터 베이스 정보

    session_start(); //세션 시작

    if(!isset($_SESSION['login_id'])) {
        //로그인하지 않은 사용자
        header("Location: login.php"); //login 화면으로 바꾼다
        exit(); //이 페이지를 바로 닫는다
    }

    //데이터베이스 연결
    $conn = mysqli_connect($host,$username,$password,$db_board);

    //데이터베이스 오류시 종료
    if(mysqli_connect_errno()) {
        die("데이터 베이스 오류: ". mysqli_connect_error());
    }

    if(isset($_GET['filename']) && isset($_GET['idx'])) {
        // 파일 이름 가져오기
        $file_name = basename(filter_var(strip_tags($_GET['filename']),FILTER_SANITIZE_SPECIAL_CHARS));
        $id = filter_var(strip_tags($_GET['idx']),FILTER_SANITIZE_SPECIAL_CHARS);
        
        $sql = "SELECT file_upload, file_name FROM $db_board WHERE board_id = ? ";
        $stmt = mysqli_prepare($conn, $sql);
        mysqli_stmt_bind_param($stmt, "i", $id);
        mysqli_stmt_execute($stmt);

        //결과 저장
        mysqli_stmt_store_result($stmt);
        //결과 바인딩
        mysqli_stmt_bind_result($stmt, $file_data, $file_name);
    
        // 파일이 존재하는지 확인
        if(mysqli_stmt_num_rows($stmt) > 0) {
            mysqli_stmt_fetch($stmt);

            // 다운로드 헤더 설정
            header('Content-Description: File Transfer');
            header('Content-Type: application/octet-stream');
            header('Content-Disposition: attachment; filename="'.$file_name.'"');
            header('Expires: 0');
            header('Cache-Control: must-revalidate');
            header('Pragma: public');
            header('Content-Length: ' . strlen($file_data));
            
            //파일 데이터 출력
            echo $file_data;

            exit;
        } else {
            echo '파일이 존재하지 않습니다.';
        }
    } else {
        echo '파일 이름이 전달되지 않았습니다.';
    }
?>

 

이렇게 글이 올라가집니다.

 

 

 

이렇게 DB까지 저장이 잘 되는 것을 확인했습니다.