2026年8月22日 星期六

Django REST API安全第一課: HTTPS、CSRF、CORS與Token

封包旅行記 EP05|API安全邊界

Django REST API安全第一課:
HTTPS、CSRF、CORS與Token

ESP32把JSON送到Django,不只要問「送到了嗎」,還要問:途中有沒有被看見或竄改?伺服器怎麼辨認裝置?瀏覽器為何擋下請求?Cookie與Token應該用哪一套防護?

一、先記住:四個機制保護的是不同問題

HTTPS保護傳輸途中,提供加密、完整性與伺服器身分驗證。
CSRF防止瀏覽器在自動攜帶Cookie時,被惡意網站借用登入身分送出操作。
CORS是瀏覽器的跨來源讀取規則,決定哪個網頁來源可讀取API回應。
Token讓API辨認呼叫者;仍須搭配權限、過期/輪替、撤銷與HTTPS。

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。

呼叫者常見驗證CSRFCORS
Django同站網頁Session Cookie修改資料時需要同來源通常不涉及
不同網域的前端SPACookie或Header Token使用Cookie驗證時仍要正確處理瀏覽器需要允許該Origin
ESP32/伺服器程式Token、API Key或更強的裝置憑證若不依賴瀏覽器自動帶Cookie,通常不是此威脅模型不由瀏覽器執行,CORS不會保護或阻擋它

三、HTTPS:先保護道路,再談通行證

HTTP明文傳輸時,Token、感測資料與控制命令可能被同網段或傳輸路徑上的攻擊者讀取或竄改。HTTPS透過TLS建立安全通道,但它不會自動判斷使用者能不能查看某座魚塭,也不會驗證JSON欄位是否合理。

ESP32驗證伺服器憑證TLS加密通道Authorization TokenDjango權限與資料驗證

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下的POSTPUTPATCHDELETE需要有效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"}'
一台裝置一個身分:不要把同一個管理員Token燒錄到所有ESP32。每台裝置使用獨立憑證,伺服器限制其可操作的device_id;外洩時才能只撤銷單一裝置。

七、常見迷思:看起來能用,實際上仍不安全

迷思問題正確觀念
用了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的安全檢查順序

  1. TLS:網址是否為HTTPS?憑證鏈、主機名稱與ESP32時間是否正確?
  2. Authentication:Header格式是否正確?Token是否有效、遭撤銷或誤寫到log?
  3. Permission:此裝置能否寫入指定device_id與API?
  4. Validation:JSON欄位、型別、長度、數值範圍與時間是否合理?
  5. Idempotency:QoS或重試造成重複請求時,event_uuid是否能去重?
  6. 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可單獨停用,服務不中斷且留下稽核紀錄

十二、延伸閱讀(官方文件)

安全設定會隨框架版本與部署平台調整;實作時應以目前使用版本的官方文件為準。

封包旅行記 EP05|真正的API安全,不是把錯誤訊息消掉,而是讓每條連線都能回答:誰在呼叫、能做什麼、資料是否安全、失敗後如何追查。

沒有留言:

張貼留言