Django REST API安全第一課:
HTTPS、CSRF、CORS與Token
ESP32把JSON送到Django,不只要問「送到了嗎」,還要問:途中有沒有被看見或竄改?伺服器怎麼辨認裝置?瀏覽器為何擋下請求?Cookie與Token應該用哪一套防護?
一、先記住:四個機制保護的是不同問題
HTTPS+身分驗證+權限+輸入驗證才是一條完整防線。CORS不是登入機制,CSRF Token也不是API存取權杖,Token更不能取代HTTPS。
二、兩條連線路徑,安全需求並不相同
瀏覽器管理介面
人員登入Session CookieCSRF
Django後台或同站儀表板通常使用Session。瀏覽器會自動帶Cookie,因此POST、PUT、PATCH、DELETE等修改操作需要CSRF防護。
ESP32裝置API
裝置身分Authorization HeaderToken
ESP32不是瀏覽器,不受瀏覽器同源政策約束;通常使用HTTPS並在Header主動攜帶裝置Token,不使用使用者Session Cookie。
| 呼叫者 | 常見驗證 | CSRF | CORS |
|---|---|---|---|
| Django同站網頁 | Session Cookie | 修改資料時需要 | 同來源通常不涉及 |
| 不同網域的前端SPA | Cookie或Header Token | 使用Cookie驗證時仍要正確處理 | 瀏覽器需要允許該Origin |
| ESP32/伺服器程式 | Token、API Key或更強的裝置憑證 | 若不依賴瀏覽器自動帶Cookie,通常不是此威脅模型 | 不由瀏覽器執行,CORS不會保護或阻擋它 |
三、HTTPS:先保護道路,再談通行證
HTTP明文傳輸時,Token、感測資料與控制命令可能被同網段或傳輸路徑上的攻擊者讀取或竄改。HTTPS透過TLS建立安全通道,但它不會自動判斷使用者能不能查看某座魚塭,也不會驗證JSON欄位是否合理。
Django正式環境安全設定示意
# settings.py(正式環境示意,需依部署架構調整)
DEBUG = False
ALLOWED_HOSTS = ["api.example.org"]
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
# 只有在「可信任的反向代理」確實設定此Header時才使用:
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
不要盲目複製:HSTS與Proxy Header設定錯誤可能造成網站無法存取或偽造HTTPS判定。應先確認Nginx、平台代理及網域均已正確提供HTTPS,再逐項啟用並測試。
ESP32端也要驗證憑證。僅使用「insecure」模式雖然畫面上有HTTPS,卻放棄了伺服器身分驗證,可能受到中間人攻擊。應正確同步時間並配置可信任CA。
四、CSRF:防止別的網站借用你的Cookie
假設教師已登入Django後台,Session Cookie保存在瀏覽器。若瀏覽器被誘導開啟惡意網站,該網站可能嘗試向Django送出「刪除資料」請求;瀏覽器可能自動附上Cookie。CSRF Token用來證明這次修改操作來自可信任頁面流程。
<form method="post">
{% csrf_token %}
<button type="submit">更新設備名稱</button>
</form>
AJAX使用SessionAuthentication時,需取得CSRF Cookie並在不安全方法的Request Header傳送X-CSRFToken。Django REST Framework官方文件指出,SessionAuthentication下的POST、PUT、PATCH、DELETE需要有效CSRF Token。
@csrf_exempt當作通用除錯方法。若真正需求是讓ESP32呼叫API,應建立清楚的Token驗證API View,而不是關掉整個網站的CSRF保護。五、CORS:瀏覽器的讀取許可,不是API門鎖
Origin由通訊協定+主機+Port組成。https://dashboard.example.org中的JavaScript呼叫https://api.example.org就是跨來源;瀏覽器可能先送出OPTIONS預檢,伺服器再以CORS Header表示是否允許。
CORS能做什麼
讓受信任的前端網頁在瀏覽器中讀取API回應;限制哪些Origin、Method與Header可使用。
CORS不能做什麼
不能阻止curl、ESP32或攻擊者伺服器直接呼叫API,也不能取代Authentication與Permission。
# pip install django-cors-headers
INSTALLED_APPS = [
# ...
"corsheaders",
]
MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware",
"django.middleware.common.CommonMiddleware",
# ...
]
CORS_ALLOWED_ORIGINS = [
"https://dashboard.example.org",
]
CORS_URLS_REGEX = r"^/api/.*$"
# 僅Cookie跨站情境才評估開啟,並搭配CSRF與Cookie策略:
CORS_ALLOW_CREDENTIALS = False
正式環境避免CORS_ALLOW_ALL_ORIGINS = True。應列出真正需要的Origin;CORS與CSRF_TRUSTED_ORIGINS是兩套不同設定,不要以為允許CORS就自動通過CSRF。
六、Token:回答「你是誰」,Permission回答「你能做什麼」
DRF內建TokenAuthentication適合教學與簡單Client–Server情境。Client以Header送出Authorization: Token <key>。官方文件也提醒,內建Token是較簡單的實作;正式系統若需要每裝置多Token、到期、輪替或細緻權限,應評估更完整的方案。
# settings.py
INSTALLED_APPS = [
# ...
"rest_framework",
"rest_framework.authtoken",
]
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.TokenAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
}
# 設定後執行:python manage.py migrate
# views.py
from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
class DeviceEventView(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
event_uuid = request.data.get("event_uuid")
if not event_uuid:
return Response({"error": "event_uuid required"}, status=400)
# 還要驗證:此Token可否代表這台device_id、欄位型別與資料範圍
return Response({"accepted": True, "event_uuid": event_uuid}, status=201)
curl -X POST "https://api.example.org/api/events/" \
-H "Authorization: Token YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"event_uuid":"DEVICE-001-0001","event_type":"STORY_START"}'
七、常見迷思:看起來能用,實際上仍不安全
| 迷思 | 問題 | 正確觀念 |
|---|---|---|
| 用了HTTPS就安全 | 任何持有Token者仍可能越權 | HTTPS之外還要驗證、權限、輸入驗證與日誌 |
| 開放CORS讓ESP32能連 | ESP32不受瀏覽器CORS限制 | 查DNS、TLS、Token、API路徑與防火牆 |
| CSRF錯誤就加csrf_exempt | 移除重要防線且混淆驗證模式 | Session走CSRF;裝置API走Header Token與Permission |
| Token放在URL比較方便 | 可能出現在日誌、瀏覽紀錄與Referer | 放在Authorization Header,且避免輸出到log |
| 前端藏起按鈕就代表沒權限 | 攻擊者可直接呼叫API | 每個API在伺服器端檢查Permission與物件所有權 |
| 所有裝置共用一把Token | 一台外洩等於整個場域失守 | 個別憑證、最小權限、可撤銷及輪替 |
八、從ESP32到Django的安全檢查順序
- TLS:網址是否為HTTPS?憑證鏈、主機名稱與ESP32時間是否正確?
- Authentication:Header格式是否正確?Token是否有效、遭撤銷或誤寫到log?
- Permission:此裝置能否寫入指定device_id與API?
- Validation:JSON欄位、型別、長度、數值範圍與時間是否合理?
- Idempotency:QoS或重試造成重複請求時,event_uuid是否能去重?
- Audit:記錄結果、裝置ID、時間與Request ID,但不得記錄完整Token。
| HTTP結果 | 判讀 | 下一步 |
|---|---|---|
401 Unauthorized | 沒有提供或無法驗證身分 | 檢查Authorization Header與Token狀態 |
403 Forbidden | 身分已知但不允許,或Session情境的CSRF失敗 | 查Permission、物件所有權及CSRF日誌 |
400 Bad Request | 資料格式或欄位驗證失敗 | 查看安全且不洩密的錯誤內容 |
| 瀏覽器顯示CORS錯誤 | 瀏覽器無法讀取回應;伺服器仍可能已收到請求 | 看Browser Network與Server log,確認Origin、OPTIONS及Header |
九、場域最低安全基線
全程HTTPS、有效憑證、ESP32驗證CA、停用明文API。
每台裝置獨立Token、最小權限、可撤銷、定期輪替。
Serializer驗證、節流、UUID去重、安全日誌、備份與更新。
祕密不進Git:使用環境變數或平台祕密管理保存Django SECRET_KEY、資料庫密碼與API憑證;Token不可寫進公開程式碼、截圖或教學log。
十、學生實作:安全不是抄設定,而是建立威脅模型
No-AI|四機制分類
針對「封包被偷看、惡意網站借Cookie、前端跨網域、ESP32身分冒用」四個情境,分別判斷HTTPS、CSRF、CORS、Token誰負責,並說明誰不能解決。
AI Pair|安全審查
提供一份去除真實密鑰的settings.py與API View,請AI列出風險;學生必須逐條對照官方文件,標示接受、修正或駁回及理由。
Challenge AI|紅藍隊驗證
測試無Token、錯Token、越權device_id、重複event_uuid、錯誤Origin與無CSRF請求;完成預期狀態碼、Server log與修補證據。
十一、上線前驗收清單
| 檢核 | 通過證據 |
|---|---|
| HTTPS與憑證驗證 | HTTP會安全導向HTTPS;ESP32拒絕錯誤憑證 |
| Session與CSRF | 合法表單可操作;缺少或錯誤CSRF的修改請求被拒絕 |
| CORS最小開放 | 只有指定Origin可由瀏覽器讀取API,且僅套用需要的路徑 |
| Token與Permission | 每台裝置獨立;不可寫入別台設備資料 |
| 不洩漏祕密 | 程式庫、log、錯誤頁與截圖均無Token或密碼 |
| 異常與撤銷演練 | 外洩Token可單獨停用,服務不中斷且留下稽核紀錄 |
十二、延伸閱讀(官方文件)
- Django:Security in Django
- Django:How to use CSRF protection
- Django REST Framework:Authentication
- Django REST Framework:Permissions
- MDN:Cross-Origin Resource Sharing
- django-cors-headers:設定文件
安全設定會隨框架版本與部署平台調整;實作時應以目前使用版本的官方文件為準。
沒有留言:
張貼留言