發現問題:明文密碼
現代人通常是一組密碼打天下,這也解釋為甚麼很多網站不是存明碼,因為一旦資料庫被攻破,駭客可以拿著明碼去試其他網站,
也因此,當你忘記密碼,他無法寄密碼回來給妳。因為資料庫中存的並不是你的明碼。
加密與 hash,不要再搞錯了,求你
加密可以解密、雜湊不行還原。
雜湊 hash(多對一)
明文 => hash => 文字,對不同的明文 hash 可能得到相同的文字,因為多對一所以無法還原。
我們可以用鴿籠原理去想像這樣的多對一關係
hash 演算法舉例:
hash 演算法 = n % 999
2 => hash => 2
1002 => hash => 2
2000 => hash => 2
不同的明文對應相同的文字,叫做碰撞。一般來說,有名的演算法發生碰撞的機率都很低。
hash 機制可以運用在密碼上,登入後輸入的密碼與資料庫 hash 過後的結果進行比對。
延伸閱讀
[資訊安全] 密碼存明碼,怎麼不直接去裸奔算了?淺談 Hash , 用雜湊保護密碼
一次搞懂密碼學中的三兄弟 — Encode、Encrypt 跟 Hash
修正問題:使用內建 hash 函式
password_hash 可以選幾種不同的演算法,要注意較舊的 php 版本不支援。
到註冊處理頁面,調整 password
$password = password_hash($_POST['password'], PASSWORD_DEFAULT);
利用 password_verify 進行密碼驗證,password_verify(傳入的密碼,驗證後的結果)
登入處理
<?php
require_once('conn.php');
session_start();
$username = $_POST['username'];
$password = $_POST['password'];
if (empty($username) || empty($password)) {
header('Location: login.php?errCode=1');
die('資料未輸入齊全');
}
$sql = sprintf('select * from users where username="%s"', $username);
$result = $conn -> query($sql);
if ($result->num_rows === 0) {
header("Location: login.php?errCode=2");
exit();
}
$row = $result->fetch_assoc();
if (password_verify($password, $row['password'])) {
$_SESSION['username'] = $username;
header('Location: main.php');
} else {
header('Location: login.php?errCode=2');
die('帳密輸入錯誤');
}
?>
註冊處理
<?php
require_once('conn.php');
session_start();
$username = $_POST['username'];
$nickname = $_POST['nickname'];
$password = password_hash($_POST['password'],PASSWORD_DEFAULT);
if (empty($username) || empty($nickname) || empty($password)) {
header('Location: register.php?errCode=1');
die('資料未輸入完整');
}
$sql = sprintf('insert into users(username, nickname, password) values("%s", "%s", "%s")', $username, $nickname, $password);
$result = $conn -> query($sql);
if (!$result) {
$code = $conn -> errno;
if ($code === 1062) {
header('Location: register.php?errCode=2');
}
die($conn->error);
}
$_SESSION['username'] = $username; //這樣註冊完成就是登入狀態
header('Location: main.php');
?>
重點回顧
- password 註冊時會經過 hash
- 登入時,會先透過 username 將 password 撈出來,所以會先檢查有無查到 user 沒有的話會錯誤返回。如果有拿到 password 將 password 拿出來,再經過 password_verify 去錯誤比對。
- password_verify 比對使用者輸入的 password 與資料庫存的 password。如果通過驗證就登入成功,否則登入失敗
優點:資料庫被偷,保護使用者的密碼
發現問題:XSS
Cross-site Scripting 在別的網站上(跨站)執行 script。舉例來說,目前留言版輸入的東西,並不是字串,會被解讀成程式碼的一部分。也就是我可以寫一串 js 將他導入釣魚網站,做一個跟原本網頁很像的網站。
透過 <script>console.log(document.cookie)</script>
可以拿到 session_id。透過這樣的方式可以拿到這個頁面使用者的 cookie ,再利用其他方式傳到我自己的 server。偷他 cookie 就可以偷他 session_id,偷他 session_id 就可以偷他身分。
修正問題:htmlspecialchars
會將一些特殊字元進行編碼。
將跳脫的內容存到資料庫,IOS、安卓看不懂這內容。所以應該要保留使用者的原貌,在顯示的時候做這件事情。要將所有使用者可以自己調控內容的地方,都做 escape。之後如果出現新的欄位,也要做這件事情。
main.php
<div class='card__text'>
<div class='card__text-title'>
<span class='nickname'><?php echo escape($row['nickname'])?> </span>
<span class='date'><?php echo $row['create_at']?></span>
</div>
<div class='card__text-content'>
<?php echo escape($row['content'])?>
</div>
</div>
utils.php
function escape($str) {
return htmlspecialchars($str, ENT_QUOTES);
}
發現問題:SQL Injection 駭客的填字遊戲
與 xcs 類似,只是用在 sql query 上面。
select 填字
這段程式碼等同 select * from users where username = 'aa' 後面都會被當作註解。
儘管我不知道 username 我也可以做相同的事情,進行登入。
例如,我可以select * from users where username = "" or 1=1 #
攻破資料庫。 1 = 1 表示 true
insert 填字
insert 可以新增多筆資料。 INSERT INTO comments(nickname, content) values('aa', 'bb'), ('cc','dd')
,知道這樣的方式,可否將原本的 query 改成我想插入的方式 ?
INSERT INTO comments(nickname, content) values('aa', 'bb'), ('cc','dd'); #利用這樣的形式,可以插入多筆資料
insert into comments(nickname, content) values('%s', '%s'); # 原本的形式
#先不要管 nickname,處理 content
insert into comments(nickname, content) values('aa', '%s');
#我們可以這樣改
insert into comments(nickname, content) values('aa', ''),('admin', 'test');
#與原本的 query 比對 => 代碼意思變成我要新增兩個內容。
%s 是 '),('admin', 'test
因為我的 query 是用字串拼接,所以任何人都可以拼拼湊湊將query變成她想要的樣子。有了這個漏洞,我們就可以模仿任何人發文。
除此之外,還可以利用 sql query 的方式去攻擊資料庫,拿到所有你想拿的資料。
例如:新增一筆資料,內容就是密碼
#這麼做,掌握 query 完全部份
insert into comments(nickname, content) values('aa', ''),('admin', 'test')#'); #米字號後面都會變成註解
#subsql,sql 裡面可以是 sql
insert into comments(nickname, content) values('aa', (select password from users limit 1));
#與 SQL injection 結合
insert into comments(nickname, content) values('aa', ''),('admin', (select password from users limit 1))#'
這個時候 content 是 '),('admin', (select password from users limit 1))#
#指定撈 user_id = 88 的密碼
insert into comments(nickname, content) values('aa', ''),('admin', (select password from users where id = 88))#'
可以透過 echo $sql;
搭配 exit()
來印出拼湊完的結果。
還可以改得更複雜,將 username 一起拿到手
現在 content :
'),((select username from users where id = 88), (select password from users where id = 88))#
資料庫中會有預設的 information schema 裡面會有 sys_tables,會知道你有哪些 table。每個 table 也有一些東西可以知道那個欄位是甚麼。或者就算沒有上述,欄位的取名也差不多,也可以用猜的。
修正問題:prepared statement
用 mysql 的內建機制,做字串拼接。有點像 xcs 用字串跳脫、字串轉移的概念。透過內建機制將這些指令解釋成字串。
先處理 handle_add_comment.php,將 sprintf() 改成下列形式
$sql ='insert into comments(nickname, content) values(?, ?)';
將 sql 指令傳入 prepare,再將參數 bind 到參數上面,最後去執行這個 query
$stmt = $conn->prepare($sql);
#看我有幾個參數,就會有幾個字。s 表示 string。
$stmt->bind_param('ss', $nickname, $content);
$result = $stmt->execute();
透過這樣的作法,之前準備的惡意字串,就不會被當作指令去執行。
全部的地方都要改,除了避免漏網之魚,也是為了追求一致性。
調整 handle_register.php
$sql = 'insert into users(username, nickname, password) values(?, ?, ?)';
$stmt = $conn->prepare($sql);
$stmt->bind_param("sss", $username, $nickname, $password);
$result = $stmt->execute();
調整 handle_login.php
$sql ='select * from users where username=?';
$stmt = $conn->prepare($sql);
$stmt->bind_param("s",$username);
$result = $stmt->execute(); #只是看有無執行成功
# 要拿到資料,必須要再加上
$result = $stmt->get_result();
調整 utils.php
function getNickname($username) {
global $conn;
$sql = 'select nickname from users where username = ?';
$stmt = $conn->prepare($sql);
$stmt->bind_param("s",$username);
$result = $stmt->execute();
$result = $stmt->get_result();
$nickname = $result->fetch_assoc()['nickname'];
return $nickname;
}
調整 main.php
$stmt = $conn->prepare('select * from comments order by id desc');
$result = $stmt->execute();
if (!$result) {
die('錯誤訊息 : ' . $conn ->error);
}
$result = $stmt->get_result();
$username = Null;
$nickname = Null;
if (!empty($_SESSION['username'])) {
$username = $_SESSION['username'];
$nickname = getNickname($username);
}