일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 쿠키
- 웹개발
- CTF
- Los
- JS
- MySQL
- file upload
- cookie 탈취
- php
- Cross Site Request Forgery
- lord of sqli
- lord of sql injection
- union sql injection
- 로그인페이지
- Python
- cors
- Reflected Xss
- 게시판 만들기
- JWT
- XSS
- 모의해킹
- sql injection point
- 세션
- 로그인
- Error based sql injection
- csrf
- 과제
- sql injection
- css
- blind sql injection
- Today
- Total
Almon Dev
게시판 만들기 #9 (게시글 검색과 정렬) 본문
게시글 검색 기능
검색어를 입력하면 서버에서 search 쿠키에 검색어를 저장한 뒤에 게시글을 불러올 때 조건문에 포함시킵니다.
쿠키 관리하는 함수
JS에는 setCookie 같은 함수가 따로 없고 document.cookie에 직접 접근해서 설정을 해야 합니다.
function setCookie(name, value) {
let date = new Date();
date.setTime(date.getTime() + 60 * 60 * 24);
document.cookie =
name +
'=' +
encodeURIComponent(value) +
'; expires=' +
date.toUTCString() +
';path=/forum/';
}
쿠키의 이름과 값을 입력받아서 쿠키를 생성하는 함수입니다.
expries : 만료기간을 뜻합니다.
path : 쿠키를 사용할 수 있는 도메인을 뜻합니다.
function findCookie(name) {
let cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
let c = cookies[i].trim();
if (c.startsWith(name + '=')) {
return true;
}
}
return false;
}
쿠키의 이름을 입력받아서 쿠키가 있는지 검사하고 true, false를 리턴하는 함수입니다.
function deleteCookie(name) {
if (findCookie(name)) {
document.cookie =
name + '=; expires=Thu, 01 Jan 1999 00:00:10 GMT; path=/forum/';
}
}
쿠키가 있는 경우 쿠키의 만료시간을 과거로 돌려서 삭제하는 함수입니다.
검색 UI 만들기
//검색 태그 생성
const searchContainer = document.createElement('div');
searchContainer.classList.add('search-container');
// select
const searchSelect = document.createElement('select');
searchSelect.classList.add('search-select');
const option1 = document.createElement('option');
option1.value = 'title';
option1.textContent = '제목';
const option2 = document.createElement('option');
option2.value = 'content';
option2.textContent = '내용';
const option3 = document.createElement('option');
option3.value = 'writer';
option3.textContent = '작성자';
searchSelect.appendChild(option1);
searchSelect.appendChild(option2);
searchSelect.appendChild(option3);
// 검색어 입력
const searchInput = document.createElement('input');
searchInput.classList.add('search-input');
// 검색 버튼
const searchSubmit = document.createElement('button');
searchSubmit.classList.add('search-submit');
searchSubmit.innerText = '검색';
searchSubmit.setAttribute(
'onclick',
`searchSubmit(${categoryId}, '${categoryName}', 1)`
);
searchContainer.appendChild(searchSelect);
searchContainer.appendChild(searchInput);
searchContainer.appendChild(searchSubmit);
게시판 만들기 #3에서 만들었던 게시글 목록을 확인하는 코드(post_list.js)에 검색창을 추가했습니다.
function searchSubmit(categoryId, categoryName, pageNum) {
const searchOptionTag = document.querySelector(
'.search-select option:checked'
);
const searchInput = document.querySelector('.search-input');
const searchOption = searchOptionTag.value;
const searchText = searchInput.value;
setCookie('search', `${searchOption}:${searchText}`);
postList(categoryId, categoryName, 1);
}
검색어를 입력하고 버튼을 누르면 쿠키를 생성하고 목록창을 새로고침 합니다.
쿠키는 검색 옵션:검색어로 저장됩니다.
쿠키를 사용하는 이유는 서버의 편의성 때문입니다.
원래 게시글을 읽어오던 페이지에서 isset문을 통해 쿠키가 존재할 경우 sql문을 조금만 수정하도록 변경했습니다.
// 만들어진 HTML
<div class="search-container">
<select class="search-select">
<option value="title">제목</option>
<option value="content">내용</option>
<option value="writer">작성자</option>
</select>
<input class="search-input">
<button class="search-submit" onclick="searchSubmit(1, '공지사항', 1)">검색</button>
</div>
위의 JS 코드가 실행되면 이런 형태의 HTML이 생성됩니다.
SQL문 수정
~~~~~~~~~~~
$sql = "select posts.post_id, posts.title, posts.writer_id, posts.created_at,
posts.views , users.user_id, users.nickname
from posts
join users on posts.writer_id = users.id
where posts.category_id=$category_id";
if (isset($_COOKIE['search'])) {
$search = explode(':', $_COOKIE['search']);
$search_option = htmlspecialchars($search[0]);
$search_text = htmlspecialchars($search[1]);
$search_filter = preg_replace('/\s+/', '', $search_text);
if ($search_filter != '') {
if ($search_option != 'writer') {
$sql = $sql . " and posts.$search_option like '%$search_text%'";
}else {
$sql = $sql . " and users.nickname like '%$search_text%'";
}
}
}
$sql = $sql . " limit $offset, $limit";
$result = runSQL($sql);
$posts = [];
if (mysqli_num_rows($result) > 0) {
while ($row = mysqli_fetch_array($result)) {
$posts[] = [
"post_id"=> $row["post_id"],
"title"=> $row["title"],
"user_id"=> $row["user_id"],
"nickname"=> $row["nickname"],
"created_at"=> $row["created_at"],
"views"=> $row["views"],
];
}
}
echo json_encode($posts);
}else {
echo "fail";
}
}else {
header("location: /login2.php");
}
if문을 통해 쿠키에 검색어가 있는지 확인하고 where문에 추가해서 검색된 게시글만을 가져오는 코드입니다.
if (isset($_COOKIE['search'])) {
$search = explode(':', $_COOKIE['search']);
isset 함수를 이용해서 search 쿠키가 확인합니다.
search 쿠키가 있다면 explode를 이용해서 쿠키의 값을 :를 기준으로 나눠 배열로 $search에 저장합니다.
$search_option = htmlspecialchars($search[0]);
$search_text = htmlspecialchars($search[1]);
검색 옵션과 검색어를 각 변수에 저장합니다.
htmlspecialchars 함수는 <'"> 같은 특수문자를 HTML Entity로 변환해서 XSS 공격을 막아줍니다.
검색기능이 XSS에 사용되는 경우는 주로 Reflected XSS입니다.
검색어가 서버에서 클라이언트로 다시 응답되는 점을 이용하는 것인데 저는 검색어를 응답에 포함시키지 않기 때문에 Reflected XSS는 불가능합니다.
제가 htmlspecialchars를 사용한 것은 게시글을 저장할 때 HTML Entity로 변환해서 저장했기 때문입니다.
$search_filter = preg_replace('/\s+/', '', $search_text);
if ($search_filter != '') {
이건 공백만 검색어에 포함된 경우를 제외하기 위한 코드입니다.
정규식을 이용해서 공백을 '' 빈 문자로 치환한 뒤 그 결과가 빈문자열이 아닌 경우에만 if문을 실행합니다.
검색어가 비어있거나 Tab Space \n처럼 공백만 검색어에 있는 경우 검색기능을 실행하지 않기 위해 추가했습니다.
if ($search_filter != '') {
if ($search_option != 'writer') {
$sql = $sql . " and posts.$search_option like '%$search_text%'";
}else {
$sql = $sql . " and users.nickname like '%$search_text%'";
}
}
검색 옵션이 writer인 경우는 작성자의 닉네임을 검색하는 것이기에 if문을 통해 분리했습니다.
like %검색어%로 검색어만 포함되어 있으면 검색되게 만들었습니다.
ex) 12 검색 => 112443
$sql = $sql . " limit $offset, $limit";
$result = runSQL($sql);
limit와 offset을 이용해서 페이지에 최대 15개의 게시글만 나오도록 설정하고 sql문을 실행합니다.
페이징 수정
if (isset($_COOKIE['search'])) {
$search = explode(':', $_COOKIE['search']);
$search_option = htmlspecialchars($search[0]);
$search_text = htmlspecialchars($search[1]);
$search_filter = preg_replace('/\s+/', '', $search_text);
if ($search_filter != '') {
if ($search_option != 'writer') {
$sql = $sql . " and posts.$search_option like '%$search_text%'";
}else {
$sql = $sql . " and users.nickname like '%$search_text%'";
}
}
}
코드는 위의 내용과 같습니다.
검색을 했을 때 10개의 결과만 가져와도 페이지가 2까지 표시되는 문제를 해결하기 위해서 추가했습니다.
검색 UI 꾸미기
.search-container {
margin-top: 1rem;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
color: #3e2c1c;
}
.search-select {
height: 25px;
padding: 3px 4px;
color: #3e2c1c;
font-size: 12px;
font-weight: 800;
background-color: rgba(200, 176, 141, 0.5);
border: 1px solid rgba(200, 200, 200, 0.3);
border-radius: 6px 0 0 6px;
}
.search-input {
height: 25px;
}
.search-submit {
height: 25px;
padding: 3px 17px;
color: #3e2c1c;
font-size: 15px;
font-weight: 800;
background-color: rgba(200, 176, 141, 0.5);
border: 1px solid rgba(200, 200, 200, 0.3);
border-radius: 0 6px 6px 0;
}
.search-submit:hover {
cursor: pointer;
text-decoration: underline;
}
CSS를 추가해서 검색창을 꾸몄습니다.
사진
정렬 기능
조회수와 작성일, 제목을 기준으로 정렬하는 기능을 만들었습니다.
정렬 UI 생성
const titleTh = document.createElement('th');
const spanTh = document.createElement('span');
spanTh.textContent = '제목▼';
spanTh.classList.add('thead-title');
spanTh.setAttribute(
'onclick',
`sort("title", ${categoryId}, "${categoryName}")`
);
titleTh.appendChild(spanTh);
const createdAtTh = document.createElement('th');
createdAtTh.classList.add('thead-date');
createdAtTh.textContent = '작성일▼';
createdAtTh.setAttribute(
'onclick',
`sort("created_at", ${categoryId}, "${categoryName}")`
);
const viewsTh = document.createElement('th');
viewsTh.classList.add('thead-views');
viewsTh.textContent = '조회수▼';
viewsTh.setAttribute(
'onclick',
`sort("views", ${categoryId}, "${categoryName}")`
);
// 추가한 CSS
// .thead-title:hover,
// .thead-date:hover,
// .thead-views:hover {
// cursor: pointer;
// text-decoration: underline;
// }
게시판 만들기 #3에서 만들었던 UI에 ▼를 정렬이 가능하는 의미로 추가했습니다.
onclick 이벤트에 추가한 sort함수는 정렬을 위한 쿠키를 생성해 주는 함수입니다.
쿠키 생성
function sort(column, categoryId, categoryName) {
if (findCookie('sort')) {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
let c = decodeURIComponent(cookies[i].trim());
if (c.startsWith('sort=')) {
let sortOrder = c.split('=')[1].split(':')[1];
if (sortOrder == 'asc') {
console.log(1);
sortOrder = 'desc';
} else {
sortOrder = 'asc';
}
setCookie('sort', `${column}:${sortOrder}`);
}
}
} else {
setCookie('sort', column + ':asc');
}
postList(categoryId, categoryName, 1);
// console.log('test');
}
sort 쿠키가 이미 있는 경우 다시 클릭을 하면 asc를 desc로 desc를 asc로 변경해 주고
sort 쿠키가 없는 경우는 asc로 생성합니다.
if (findCookie('sort')) {
const cookies = document.cookie.split(';');
sort 쿠키가 있는 경우 cookies 변수에 쿠키를 배열로 저장합니다.
document.cookies로 쿠키를 가져올 경우 ;로 각 쿠키를 구분합니다.
for (let i = 0; i < cookies.length; i++) {
let c = decodeURIComponent(cookies[i].trim());
if (c.startsWith('sort=')) {
구분한 쿠키들을 돌면서 "sort=" 문자열로 시작하는 쿠키를 찾습니다.
let sortOrder = c.split('=')[1].split(':')[1];
if (sortOrder == 'asc') {
console.log(1);
sortOrder = 'desc';
} else {
sortOrder = 'asc';
}
setCookie('sort', `${column}:${sortOrder}`);
= 뒤에 있는 쿠키의 값을 가져오고 :를 기준으로 분리한 뒤 1번 인덱스의 값을 가져옵니다.
ex) title:asc => asc 가져옴
가져온 값이 asc면 desc로 desc면 asc로 변경합니다.
} else {
setCookie('sort', column + ':asc');
}
postList(categoryId, categoryName, 1);
sort 쿠키가 없는 경우 쿠키를 생성합니다.
SQL문 수정
if (isset($_COOKIE['sort'])) {
$sort = explode(':', $_COOKIE['sort']);
if ($sort[1] === 'asc' || $sort[1] === 'desc') {
$sql = $sql . " order by posts.$sort[0] $sort[1]";
}else {
$sql = $sql . " order by posts.created_at desc";
}
}else {
$sql = $sql . " order by posts.created_at desc";
}
$sql = $sql . " limit $offset, $limit";
$result = runSQL($sql);
if문을 통해 sort쿠키가 있는 경우 order by문을 추가해 정렬을 실행합니다.
if (isset($_COOKIE['sort'])) {
$sort = explode(':', $_COOKIE['sort']);
sort 쿠키가 있는지 확인하고 있을 경우 쿠키를 가져와 :를 기준으로 분리합니다.
if ($sort[1] === 'asc' || $sort[1] === 'desc') {
$sql = $sql . " order by posts.$sort[0] $sort[1]";
}else {
$sql = $sql . " order by posts.created_at desc";
}
분리한 쿠키의 1번 인덱스 값이 asc 혹은 desc이면 order by문에 추가하고 그렇지 않을 경우에는 생성일 기준으로 정렬합니다.
sql injection을 배울 때 order by 구문은 prepared statement를 사용하지 못해 취약하다는 말을 들었습니다.
그래서 $sort [1]의 값을 지정해서 asc, desc인 경우만 sql문에 추가했습니다.
column명인 $sort [0] 같은 경우는 따로 검사를 하지 않기 때문에 sql injection이 가능한 취약점입니다.
사진
마무리
점점 코드가 복잡해지고 반복되는 구간이 많다는 것이 느껴집니다.
각 기능에 코드가 너무 섞여서 정리가 잘 안 됩니다.
'모의해킹 > 웹 개발' 카테고리의 다른 글
게시판 만들기 #10 (게시글 내부에 이미지 추가) (0) | 2025.02.13 |
---|---|
게시판 만들기 #7 (아이디 비밀번호 찾기) (0) | 2025.01.22 |
게시판 만들기 #6 (게시글 삭제하기) (0) | 2024.11.12 |
게시판 만들기 #5 (게시글 수정하기) (0) | 2024.11.12 |
게시판 만들기 #4 (게시글 읽기) (0) | 2024.11.12 |