iT邦幫忙

2026 iThome 鐵人賽

DAY 27
0

上一篇我們加上了身分驗證,至少不是任何人都能動我們的資料了。

但安全不是只有「誰可以進來」而已。

今天要看的是一個很經典、而且到現在都還一直在發生的問題:

SQL Injection(SQL 注入)。

先說一件很重要的事:

今天的內容是在自己的電腦、自己建立的練習資料庫上示範,目的是理解原理,才知道要怎麼防。
拿這些方法去測試別人的網站是違法的,不要這樣做。

問題出在哪裡?

回想 Day 18,我們是這樣跟資料庫溝通的:

SELECT * FROM tasks WHERE id = 1;

SQL 是一段「文字」,資料庫收到之後會去解析它,判斷哪些是指令、哪些是資料。

而我們寫程式的時候,常常會需要把使用者輸入的東西放進 SQL 裡。

如果是用字串拼接的方式:

sql = "SELECT * FROM task WHERE name = '" + name + "'"

或是用 f-string:

sql = f"SELECT * FROM task WHERE name = '{name}'"

問題就來了。

因為使用者輸入的內容,會直接變成 SQL 的一部分。

本來我們以為使用者會輸入「資料」,但他其實可以輸入「語法」。

實際做一次

新增一個練習用的檔案:

injection_demo.py

import sqlite3
conn = sqlite3.connect(':memory:') # 只存在記憶體,執行完就沒了
cursor = conn.cursor()

cursor.execute("""
CREATE TABLE user (
    id INTEGER PRIMARY KEY,
    username TEXT,
    password TEXT
)
""")

cursor.execute("INSERT INTO user (username, password) VALUES ('jolin', 'my-password')")
cursor.execute("INSERT INTO user (username, password) VALUES ('admin', 'super-secret')")
conn.commit()

def login(username, password):
    # 危險示範:把輸入直接拼進 SQL(實際開發絕對不要這樣寫)
    sql = f"SELECT * FROM user WHERE username = '{username}' AND password = '{password}'"

    print("實際執行的 SQL:")
    print(sql)

    result = cursor.execute(sql).fetchall()

    if result:
        print("登入成功:", result)
    else:
        print("登入失敗")

    print("-" * 50)

print("【情況 1】正常登入")
login("jolin", "my-password")
print("【情況 2】密碼打錯")
login("jolin", "wrong-password")
print("【情況 3】在密碼欄位動手腳")
login("jolin", "' OR '1'='1")
conn.close()

這裡故意把 SQL 印出來,因為重點就是看「拼出來的 SQL 長什麼樣子」。

執行:

python injection_demo.py

執行 injection_demo.py 的完整輸出

https://ithelp.ithome.com.tw/upload/images/20261011/20183865TDgQaRxaB3.png

結果是什麼?

情況 1,正常登入,SQL 是:

SELECT * FROM user WHERE username = 'jolin' AND password = 'my-password'

登入成功: [(1, 'jolin', 'my-password')]

沒問題。

情況 2,密碼打錯:

SELECT * FROM user WHERE username = 'jolin' AND password = 'wrong-password'

登入失敗

也正常。

情況 3 就是重點了。

我們在密碼欄位輸入:

' OR '1'='1

拼出來的 SQL 會變成:

SELECT * FROM user WHERE username = 'jolin' AND password = '' OR '1'='1'

結果是:

登入成功: [(1, 'jolin', 'my-password'), (2, 'admin', 'super-secret')]

明明密碼是錯的,卻登入成功了。

而且仔細看回傳的內容會發現更可怕的事:

它不只讓我們登入成功,還一次把整張 user 表的資料都撈出來了,
包含 admin 的密碼 super-secret。

我們只是在密碼欄位多打了幾個字而已。

為什麼會這樣?

把那句 SQL 拆開來看:

原本我們想要的是:

username = 'jolin' 而且 password = '(使用者輸入的密碼)'

但使用者輸入的第一個字元是單引號 ',
這個單引號把「密碼字串」提早結束掉了。

後面的 OR '1'='1 就不再是資料,而是變成 SQL 的語法。

整句的意思變成:

(username = 'jolin' 而且 password = '')或者('1' = '1')

而 '1'='1' 永遠成立。

所以不管前面怎麼樣,整個條件都會是真,資料庫就把「所有」資料回傳了。

這就是 SQL Injection 的核心:

使用者輸入的「資料」被當成「指令」執行了。

還可以做到什麼?

這個例子只是登入繞過,實際上可以做的事情更多:

看到不該看的資料
剛剛就示範了,整張使用者表被撈出來。如果裡面是真實帳號,那就是整批外洩。

用 UNION 把其他表的資料接進來
例如原本只是查商品,卻被接上使用者表,把帳號密碼一起查出來。

修改或刪除資料
如果程式執行的是 UPDATE 或 DELETE,就可能被改成刪掉整張表。

有一張很有名的漫畫(xkcd「Little Bobby Tables」),
講一個小孩的名字叫做:

Robert'); DROP TABLE students;--

學校把名字直接拼進 SQL,結果整張學生表就被刪掉了。

用「猜」的把資料問出來
就算畫面上看不到資料,也可以透過「回應有沒有變化」、「反應快不快」來一個字元一個字元試出內容。
這叫做 Blind SQL Injection。

我們自己的 API 有風險嗎?

好消息是:目前沒有。

因為我們用的是 ORM:

db.session.scalars(db.select(Task).filter_by(name=name)).all()

SQLAlchemy 會自動把 name 當成「參數」處理,不會拼進 SQL 語法裡。

但是要注意,用了 ORM 不代表一定安全。

如果哪天寫出這種程式:

from sqlalchemy import text

# 危險寫法
sql = text(f"SELECT * FROM task WHERE name = '{name}'")
db.session.execute(sql)

一樣會有 SQL Injection 的風險。明天會實際跑給大家看。

所以真正的重點不是「有沒有用 ORM」,而是:

有沒有把使用者的輸入直接拼進 SQL 字串裡。

那我把單引號過濾掉不就好了?

這是很多人第一個想到的做法,但並不可靠。

原因有幾個:

情況比想像中多
數字欄位不需要單引號也能注入,例如 /tasks?id=1 OR 1=1。

繞過的方法很多
不同資料庫的跳脫方式、編碼方式都不一樣,
可以用註解('--'、'/**/')、用不同編碼、用大小寫變形來閃過檢查。

自己寫的規則很難周全
今天想到單引號,明天想到分號,永遠會漏掉某一種。

所以正確的方向不是「把壞字元挑掉」,
而是從一開始就讓資料庫知道「這是資料,不是語法」。

這就是下一篇要講的參數化查詢。

另外一個容易被忽略的地方

Day 24 有提過,錯誤訊息不要直接丟給使用者。

在 SQL Injection 的情境下,這件事特別重要。

因為如果程式把資料庫的錯誤原文回傳出去,例如:

no such column: xxx
near "'": syntax error

攻擊者就可以從這些訊息推測出:

用的是哪一種資料庫
表格和欄位叫什麼名字
SQL 是怎麼組出來的

等於一步一步被引導著找到攻擊方式。

所以 Day 24 那句話再說一次:

詳細的錯誤記在伺服器裡自己看,回給使用者的只要一句「發生錯誤」就好。

今天認識了:

SQL Injection 的原理:使用者的輸入被當成 SQL 語法執行
用 ' OR '1'='1 實際做了一次登入繞過,而且整張表都被撈出來
危害包含資料外洩、資料被改、被刪
ORM 預設是安全的,但只要用字串拼 SQL,一樣會中
過濾特殊字元不是正確的解法
錯誤訊息也可能變成線索

原理知道了,接下來就是怎麼防。

下一篇就來看參數化查詢、ORM,還有輸入驗證要怎麼搭配使用。


上一篇
Day 26|API 安全入門
下一篇
Day 28|怎麼避免 SQL Injection?
系列文
從零開始的後端開發:用 Flask 實作 REST API,搞懂 API 與資料庫之間如何協作 共 29 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言