Learning Objectives
- 說明 mysql_secure_installation 做了哪些事,以及 Ubuntu 上 root 用 auth_socket 登入的意義
- 找出並用 DROP USER 移除匿名帳號,說明匿名帳號為什麼會讓其他人登入成錯的帳號
- 依最小權限原則替應用程式建立帳號,不讓程式使用 root
- 說出 FILE、PROCESS、WITH GRANT OPTION 的風險,以及 secure_file_priv 如何限制檔案讀寫
- 用 bind-address、防火牆與密碼政策保護連線,並用參數化查詢防止 SQL Injection
資料庫被入侵的常見途徑
資料庫通常存著整個系統最有價值的資料:會員帳號、訂單、成績。實際發生的資料外洩,多半不是因為高深的技術,而是幾個基本設定沒做好:
| 途徑 | 例子 | 本課的對策 |
|---|---|---|
| 沒有密碼或密碼太弱的帳號 | 教學用安裝包的 root 沒有密碼、留著匿名帳號 | 執行 mysql_secure_installation、移除匿名帳號、密碼政策 |
| 權限太大的帳號 | 網站程式用 root 連線,程式一有漏洞,整台資料庫都被拿走 | 最小權限原則 |
| 資料庫直接暴露在網路上 | 3306 連接埠對整個網際網路開放,被自動化程式不斷猜密碼 | bind-address、防火牆 |
| 程式把使用者輸入直接拼進 SQL | 登入表單輸入 ' OR '1'='1 就登入成功 |
參數化查詢 |
投影片使用的 XAMPP 是方便教學的安裝包,root 預設沒有密碼,投影片也提醒「除了教學實驗環境外,建議設密碼」。 本課以 Ubuntu 24.04 的 MySQL 8.0 為準,它的預設值已經比當年安全很多,但仍然有幾件事要自己做。
root 帳號與 mysql_secure_installation
Ubuntu 用 apt install mysql-server 安裝後,root@localhost 預設使用 auth_socket 驗證:
不檢查密碼,而是檢查「連線的作業系統使用者是不是 root」。所以要用 sudo mysql 登入,一般使用者即使知道帳號名稱也登不進去,
而且 root 只能從本機登入。這比「root 設一組密碼」更安全,不要為了方便把它改成密碼登入,更不要建立 'root'@'%'。
mysql_secure_installation 是 MySQL 附的安全設定精靈,安裝後執行一次,它會依序詢問:
- 是否啟用 VALIDATE PASSWORD 元件:之後設定的密碼必須達到長度、大小寫、數字、符號等強度要求
- 設定 root 密碼(Ubuntu 的 root 使用 auth_socket,會略過這一步)
- 移除匿名帳號
- 禁止 root 從其他主機登入
- 刪除任何人都能存取的 test 資料庫
- 重新載入權限表
投影片的舊寫法 set password for root@localhost = password('…') 在 8.0 已經不能用(PASSWORD() 函式已移除)。
需要替帳號改密碼時用 ALTER USER '帳號'@'主機' IDENTIFIED BY '新密碼';,改自己的密碼可以寫 ALTER USER USER() IDENTIFIED BY '…';。
導覽:執行 mysql_secure_installation
在剛安裝好 MySQL 的 Ubuntu 伺服器上執行。輸出有節錄,「…」代表省略的說明文字;帳號清單是示意,依安裝版本可能略有不同。
-
1
啟動精靈並啟用密碼強度檢查
選擇密碼強度等級:0 LOW 只檢查長度、1 MEDIUM 還要求數字、大小寫與特殊符號、2 STRONG 再加上字典檢查。一般選 1。
-
2
移除匿名帳號、禁止 root 遠端登入、刪除 test 資料庫
這幾題都回答
y。Ubuntu 的 MySQL 8.0 安裝後本來就沒有匿名帳號與 test 資料庫,但回答 y 可以確保它們不存在。 -
3
檢查有哪些帳號
root的 plugin 是auth_socket。debian-sys-maint是 Ubuntu 套件維護用的帳號,密碼存在只有 root 能讀的/etc/mysql/debian.cnf;mysql.開頭的是 MySQL 內部帳號,已經鎖定、無法登入。user 欄位是空字串的列就是匿名帳號,這裡沒有。
匿名帳號為什麼危險
匿名帳號是使用者名稱為空字串的帳號,例如 ''@'localhost':任何人用任何名稱都能以它登入。
MySQL 5 時代的安裝程式(包括 XAMPP)常會建立匿名帳號,方便測試。
匿名帳號還有一個不容易察覺的問題。MySQL 收到連線時,會把 mysql.user 依「主機寫得越具體越優先」排序,再找第一個符合的帳號。
假設有兩個帳號:'laura'@'%' 和 ''@'localhost'。laura 從本機登入時,localhost 比 % 具體,
所以 MySQL 選中的是匿名帳號 ''@'localhost',不是 laura 自己的帳號。laura 會發現自己的密碼不對、或是登入後沒有任何權限。
登入後執行 SELECT CURRENT_USER(); 可以看到伺服器實際選中的帳號(USER() 則是你連線時報的名稱)。
移除匿名帳號: 投影片的寫法是直接刪除權限表格的資料列:
DELETE FROM mysql.user WHERE User = ''; -- 舊寫法
DELETE FROM mysql.db WHERE User = '';
FLUSH PRIVILEGES;
MySQL 8.0 不建議直接修改權限表格:權限分散在 user、db、tables_priv 等多張表格,漏刪一張就會留下殘餘的權限,而且忘了 FLUSH PRIVILEGES 就不會生效。
8.0 一律用 DROP USER,它會一次清掉這個帳號在所有權限表格的資料,並且立即生效:
SELECT user, host FROM mysql.user WHERE user = ''; -- 先找出有哪些匿名帳號
DROP USER ''@'localhost'; -- 每一個都用 DROP USER 刪除
找出並移除匿名帳號
MySQL 8.0 不會自動建立匿名帳號,所以範例先在第 1 行建立一個,模擬從舊版升級上來、還留著匿名帳號的伺服器。注意 mysql.user 的結果裡,匿名帳號的 user 欄位是空白的。
最小權限原則:應用程式不用 root
最小權限原則(principle of least privilege):每個帳號只給完成工作所需的最少權限。 網站程式在日常運作中只需要讀寫資料,不需要刪除表格、建立帳號、讀寫伺服器檔案,所以替它建立專用帳號:
CREATE USER 'webapp'@'localhost' IDENTIFIED BY '…一組長而隨機的密碼…';
GRANT SELECT, INSERT, UPDATE, DELETE ON shop.* TO 'webapp'@'localhost';
- 範圍限定在程式自己的資料庫(
shop.*),不要用*.* - 主機寫程式所在的主機:程式和 MySQL 在同一台就用
localhost,在另一台就寫那台的 IP,不要用% - 建立表格、修改表格結構(部署新版本時)另外用一個管理用的帳號,不讓日常執行的程式擁有
CREATE、DROP、ALTER - 只讀報表的程式只給
SELECT - 每支程式各用一個帳號,出事時可以只停用那一個
這樣即使程式出現 SQL Injection 之類的漏洞,攻擊者也刪不掉表格、拿不到其他資料庫的資料、讀不到伺服器的檔案。
應用程式帳號能做與不能做的事
root 替網站程式建立 webapp 帳號,只給 employee 資料庫的四種資料操作權限。從第 5 行起以 webapp 身分連線(真實環境是程式用這組帳號密碼連線)。
想一想:帳號與權限
網站程式和 MySQL 在同一台主機,程式只需要查詢、新增與修改 shop 資料庫的資料。哪一個帳號設定最適當?
webapp 帳號擁有 GRANT SELECT, INSERT, UPDATE, DELETE ON employee.*。攻擊者透過程式漏洞,以 webapp 的身分執行了下面的敘述,結果是什麼?
DROP TABLE employee.assignment;
伺服器上有 'laura'@'%' 和匿名帳號 ''@'localhost' 兩個帳號。laura 在資料庫伺服器本機執行 mysql -u laura -p,伺服器會用哪個帳號比對她的登入?
要特別小心的權限:FILE、PROCESS、WITH GRANT OPTION
投影片列出三種要謹慎授予的權限:
| 權限 | 能做什麼 | 風險 |
|---|---|---|
FILE |
SELECT … INTO OUTFILE 把資料寫成伺服器上的檔案;LOAD DATA INFILE、LOAD_FILE() 讀取伺服器上的檔案 |
讀取 /etc/passwd 等系統檔案、把資料庫內容整批匯出,甚至寫入檔案 |
PROCESS |
SHOW PROCESSLIST 看到所有連線正在執行的 SQL |
別人查詢裡的資料(例如 WHERE 條件中的身分證字號)會被看到 |
WITH GRANT OPTION |
把自己的權限轉授給別人 | 權限擴散,很難追蹤最後落到誰手上 |
此外,ALL PRIVILEGES ON *.*、CREATE USER、SUPER 與 SYSTEM_VARIABLES_ADMIN 這類管理權限,也只給資料庫管理員。
沒有 PROCESS 權限的帳號執行 SHOW PROCESSLIST,只會看到自己的連線(下面是本機真實 MySQL 8.0.44 的結果,Id 與連接埠是示意):
mysql> SHOW PROCESSLIST;
+----+------+-----------------+----------+---------+------+-------+------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+------+-----------------+----------+---------+------+-------+------------------+
| 23 | fred | localhost:59701 | employee | Query | 0 | init | SHOW PROCESSLIST |
+----+------+-----------------+----------+---------+------+-------+------------------+
secure_file_priv 限制檔案讀寫的位置。 即使帳號有 FILE 權限,MySQL 8.0 也只允許在 secure_file_priv 指定的資料夾裡讀寫檔案:
| secure_file_priv 的值 | 意思 |
|---|---|
一個資料夾,例如 /var/lib/mysql-files/ |
只能在這個資料夾裡讀寫(Ubuntu 的預設) |
| 空字串 | 任何 mysqld 有權限的位置都可以,不安全 |
NULL |
完全禁止匯入匯出檔案 |
它是唯讀變數,只能在設定檔裡修改並重新啟動。寫到其他位置會出現 ERROR 1290;檔案已經存在時出現 ERROR 1086,MySQL 不會覆蓋既有的檔案。
FILE 權限與 secure_file_priv
執行前已經建立 fred,只有 SELECT ON employee.*。fred 想把資料匯出成檔案,接著 root 試著寫到不同的位置。(模擬環境的檔案只存在記憶體裡,secure_file_priv 模擬 Ubuntu 的預設值。)
密碼與驗證方式
MySQL 不保存密碼本身,只保存雜湊值。 MySQL 8.0 預設的驗證方式是 caching_sha2_password(SHA-256 加上隨機的 salt),
存在 mysql.user 的 authentication_string 欄位(MySQL 5.6 以前的欄位叫 Password)。舊的 mysql_native_password 用的是不加 salt 的 SHA-1,
8.0.34 起列為不建議使用,8.4 預設停用,9.0 移除。舊程式或舊驅動程式連不上 8.0 時,應該更新驅動程式,而不是把帳號改回 mysql_native_password。
投影片提到「密碼可用 MD5() 或 CRYPT() 加密」,這是指在自己的應用程式裡保存使用者密碼。MD5 現在算得太快、容易被暴力破解, 應用程式應該使用專門的密碼雜湊函式庫(例如 bcrypt、Argon2),不要用 MD5 或 SHA 直接雜湊。
MySQL 8.0 可以替帳號加上密碼政策:
CREATE USER 'amy'@'localhost' IDENTIFIED BY 'Amy!2026x'
PASSWORD EXPIRE INTERVAL 90 DAY -- 90 天後要求更換密碼
FAILED_LOGIN_ATTEMPTS 3 PASSWORD_LOCK_TIME 1; -- 連續輸錯 3 次,鎖定 1 天
ALTER USER 'amy'@'localhost' ACCOUNT LOCK; -- 暫時停用帳號(不刪除)
ALTER USER 'amy'@'localhost' ACCOUNT UNLOCK;
不要把密碼寫在指令裡。 mysql -u fred -pFr3d!2026 會讓密碼留在 shell 的指令紀錄,同一台主機的其他使用者也可能從程序清單看到;
mysql 本身也會警告 mysql: [Warning] Using a password on the command line interface can be insecure.。
用 -p 後面不接密碼、讓它提示輸入;需要自動登入時,把帳號密碼寫在 ~/.my.cnf 的 [client] 區段並 chmod 600,
或用 mysql_config_editor set --login-path=… 存成加密的 ~/.mylogin.cnf。程式的資料庫密碼放在設定檔或環境變數,不寫進程式碼、不上傳到 Git。
網路存取:bind-address 與防火牆
上一課看過 Ubuntu 預設 bind-address = 127.0.0.1:MySQL 只接受本機的連線,網路上的其他主機連 3306 連接埠會被拒絕。
網站程式和 MySQL 在同一台主機時,維持這個設定就好。
程式在另一台主機時,才需要開放網路連線,而且要層層限制:
bind-address改成資料庫主機在內部網路的 IP(例如10.0.0.10),不要用0.0.0.0(所有網路介面)- 防火牆只允許程式所在的主機連 3306,例如
sudo ufw allow from 10.0.0.5 to any port 3306 proto tcp;3306 不要對整個網際網路開放 - 帳號的主機寫成程式主機的 IP:
'webapp'@'10.0.0.5',不要用'webapp'@'%' - 跨主機的連線要加密:MySQL 8.0 安裝時會自動產生 TLS 憑證,可以用
ALTER USER 'webapp'@'10.0.0.5' REQUIRE SSL;規定這個帳號只能用加密連線
投影片中「建立可由另一台主機登入本機的帳號」的寫法 grant all on *.* to luke identified by 'password',
在 8.0 不但是語法錯誤,它的意思(luke 省略主機='luke'@'%',再給全部權限)也正好是上面每一點的反例。
SQL Injection:問題出在程式怎麼組 SQL
前面都在設定 MySQL 本身,但很多資料外洩發生在應用程式。假設網站的登入程式(Python)這樣寫:
# 錯誤示範:把使用者輸入的字串直接拼進 SQL
sql = "SELECT id, username FROM member WHERE username = '" + username + "' AND pin = '" + pin + "'"
cursor.execute(sql)
使用者正常輸入 alice 與 1234,送到 MySQL 的是:
SELECT id, username FROM member WHERE username = 'alice' AND pin = '1234'
但如果有人在兩個欄位都輸入 x' OR '1'='1,拼出來的 SQL 變成:
SELECT id, username FROM member WHERE username = 'x' OR '1'='1' AND pin = 'x' OR '1'='1'
輸入裡的單引號提早結束了字串,後面的 OR '1'='1' 變成 SQL 的一部分,而 '1'='1' 永遠成立,所有會員都被查出來,程式以為登入成功。
這就是 SQL Injection:使用者的輸入被當成 SQL 程式碼執行。
解法是參數化查詢(parameterized query):SQL 的結構先寫好,使用者輸入只當作「值」另外傳給資料庫驅動程式,由它負責正確地處理引號:
# 正確:用 %s 佔位,值放在第二個參數(PyMySQL、mysql-connector 的寫法)
sql = "SELECT id, username FROM member WHERE username = %s AND pin = %s"
cursor.execute(sql, (username, pin))
驅動程式會把輸入裡的單引號跳脫,送出的 SQL 中 x' OR '1'='1 只是一個奇怪的使用者名稱,查不到任何人。
注意:%s 要交給 execute() 的第二個參數處理,不能自己用 Python 的 % 或 f-string 把值填進字串,那樣仍然是拼接。
參數化查詢解決的是程式的問題,最小權限是第二道防線:即使某處漏掉了,攻擊者能做的事也被帳號的權限限制住。
同一個輸入,拼接與參數化的差別
這三個查詢就是上面三種情況送到 MySQL 的 SQL。執行前已經建立 shop.member,有 alice、bob、carol 三位會員(為了範例簡化,PIN 以明文存放;實際系統要存雜湊值)。觀察第二個查詢的 WHERE 那一步:AND 先於 OR 計算,條件變成「username = 'x'」或「'1'='1' 且 pin = 'x'」或「'1'='1'」,最後一項對每一列都成立。
想一想:檔案權限與 SQL Injection
root 在 Ubuntu 預設設定(secure_file_priv 是 /var/lib/mysql-files/)的 MySQL 執行下面的匯出,結果是什麼?
SELECT * FROM employee.client INTO OUTFILE '/home/ubuntu/client.txt';
下面哪一段 Python 程式碼可以防止 SQL Injection?(cursor 是 PyMySQL 的游標)
member 有 alice、bob、carol 三位會員。登入程式用字串拼接,攻擊者在帳號欄輸入 carol、在 PIN 欄輸入 x' OR '1'='1,拼出的 SQL 如下。會傳回幾列?
SELECT COUNT(*) FROM member WHERE username = 'carol' AND pin = 'x' OR '1'='1';
動手寫:購物網站的最小權限帳號
購物網站的程式只會查詢、新增與修改客戶資料(employee.client 表格),不會刪除客戶。請以 root 身分:
- 建立帳號
'shop_app'@'localhost',密碼Sh0p!2026 - 只授權
employee.client表格的SELECT、INSERT、UPDATE - 改用 shop_app 連線(
-- @user shop_app@localhost),依 clientID 排序查詢clientID、name - 再試著刪除 clientID 為 2 的客戶,確認會被擋下
重點整理:安全檢查清單
| 項目 | 做法 |
|---|---|
| 安裝後 | 執行 sudo mysql_secure_installation:密碼強度、移除匿名帳號與 test 資料庫、禁止 root 遠端登入 |
| root | Ubuntu 預設 auth_socket,用 sudo mysql 登入;不建立 'root'@'%',應用程式不用 root |
| 匿名帳號 | SELECT user, host FROM mysql.user WHERE user = '' 檢查,用 DROP USER ''@'主機' 移除(不要直接 DELETE 權限表格) |
| 應用程式帳號 | 最小權限:只給需要的權限、只到自己的資料庫、主機寫程式所在的主機 |
| 危險權限 | FILE、PROCESS、WITH GRANT OPTION、ALL ON *.* 只給管理員 |
| 檔案 | secure_file_priv 維持在專用資料夾,不設成空字串 |
| 密碼 | 8.0 預設 caching_sha2_password;密碼不寫在指令與程式碼裡;可加到期與輸錯鎖定政策 |
| 網路 | bind-address = 127.0.0.1;必須開放時只綁內部 IP、防火牆只允許特定主機、REQUIRE SSL |
| 程式 | 一律用參數化查詢,不用字串拼接 SQL |
已在 MySQL 8.0 移除、不要再用的寫法:GRANT … IDENTIFIED BY、SET PASSWORD = PASSWORD('…');直接 DELETE FROM mysql.user 加上 FLUSH PRIVILEGES 則是不建議的做法。