iT邦幫忙

2025 iThome 鐵人賽

DAY 14
0
Security

Izumi從零開始的30日WEB馬拉松系列 第 14

Day-14 認識JWT與JSON

  • 分享至 

  • xImage
  •  

今天我們要來認識JWT,這個東西算是與Session類似,很多的API、行動裝置的登入或CTF題目中都會用到。所以我們今天要來學JWT的基本攻防。只不過我們在學JWT之前要先把JSON的基礎補起來。

什麼是JSON?

JSON的中文全名為JavaScripts物件表示法,英文則為JavaScripts Object Notation,是一種非常常見的資料交換格式,幾乎所有API幾乎都會用它,形式就像JavaScripts的物件,但是只會用來表示資料。

舉例:
{
  	"user": "alice",
  	"role": "admin",
  	"age": 20,
  	"active": true
}

雙引號內的是key也就是object裡的欄位名稱,用來標示一個對應的value(值),而value可以是字串、數字、布林值、陣列、物件等。

以上大概是JSON的基本格式,其中JWT內的Header及Payload就是JSON的格式,下面我們會再詳細介紹。

什麼是JWT?

JWT是JSON Web Token,是一種用來安全傳遞使用者身分資訊的「字串」,特點是由伺服器簽名以確保不可竄改且存在用戶端內(例如:Cookie),而每次在請求時會帶上JWT(通常在Authorization header裡,Authorization是HTTP Header裡的一個欄位用途是告訴伺服器我是誰,通常傳遞身分驗證的資訊)

舉例:
	Authorization: Bearer <JWT_TOKEN>

JWT的結構

JWT由三個部份來組成,使用” . ” 來分隔,格式為:

Header.Payload.Signature

1.Header

我們會在Header裡面宣告使用的演算法(alg)與Token類型(typ)

舉例:
{
  "alg": "RS256",
  "typ": "JWT"
}

2.Payload

用來存放使用者資訊,不加密,只是Base64編碼

舉例:
{
  "user": "admin",
  "role": "admin",
  "exp": 1736054400
}

3.Signature

簽名密鑰,使用演算法加密,用來驗證是否被竄改

舉例:
RSASHA256( base64url(header) + "." + base64url(payload), secret )

以上就是JWT的格式,不知道各位是否已經找到可以攻擊的漏洞了呢?下面我來介紹一下常見的攻擊方式。

常見的攻擊方式

1.弱密鑰(Weak Secret)

若是伺服器的簽名密鑰過於簡單,攻擊者可以透過暴力破解簽名,偽造合法的JWT。

2.無簽名攻擊(alg:none)

舊版的JWT套件可以將alg設定為none,若是我們將alg設定為none的話伺服器能跳過驗證簽名,導致攻擊者能隨意偽造Payload。

3.演算法混淆攻擊

如果伺服器支援多種演算法(例如RS256或HS256),攻擊者可以將RS256的公鑰當成HS256的密鑰使用,從而偽造Token(密碼學不在我們這次的學習範圍內,如果對RSA的原理及HMAC的原理請詳見Wiki,上面將原理敘述的非常清楚)

4.Payload Manipulation

如果Payload沒有加密,只有Base64編碼的話攻擊者可以解碼Payload修改身分,例如將使用者改為管理員,如果伺服器沒有驗證簽名的話就能取得管理員身分。

防禦方法

1.使用強密鑰(256位以上的隨機字串)
2.不要將alg設定為none,並限定使用演算法
3.正確處理加密演算法,不混用
4.Payload不存放敏感資訊
5.Token設定期限,避免長時間有效
6.搭配HTTPS,避免Token被監聽

今日加碼

配合下面的Python程式碼,可以了解JWT的實際運作方式,其中使用了HS256演算法作為簽名,程式碼在下方,希望能幫到各位更加理解JWT的運作方式。

import base64
import json
import hmac
import hashlib

# Base64Url 編碼(去掉 =,並替換 +/)
def base64url_encode(data: bytes) -> str:
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode('utf-8')

# Base64Url 解碼
def base64url_decode(data: str) -> bytes:
    padding = '=' * (4 - (len(data) % 4))
    return base64.urlsafe_b64decode(data + padding)

# 建立 JWT
def create_jwt(payload: dict, secret: str) -> str:
    header = {"alg": "HS256", "typ": "JWT"}
    
    header_b64 = base64url_encode(json.dumps(header).encode())
    payload_b64 = base64url_encode(json.dumps(payload).encode())
    
    signature = hmac.new(
        secret.encode(),
        f"{header_b64}.{payload_b64}".encode(),
        hashlib.sha256
    ).digest()
    
    signature_b64 = base64url_encode(signature)
    
    return f"{header_b64}.{payload_b64}.{signature_b64}"

# 驗證 JWT
def verify_jwt(token: str, secret: str) -> dict | None:
    try:
        header_b64, payload_b64, signature_b64 = token.split('.')
        signature_check = hmac.new(
            secret.encode(),
            f"{header_b64}.{payload_b64}".encode(),
            hashlib.sha256
        ).digest()
        
        if base64url_encode(signature_check) != signature_b64:
            return None  # 驗證失敗
        
        payload = json.loads(base64url_decode(payload_b64))
        return payload
    except Exception:
        return None


# 測試
secret = "my_secret"
payload = {"user": "Izumi", "role": "admin"}

jwt_token = create_jwt(payload, secret)
print("JWT:", jwt_token)

decoded_payload = verify_jwt(jwt_token, secret)
print("驗證結果:", decoded_payload)

今日小結

今天我們學了JSON與JWT的格式,可攻擊的基本漏洞及基本的防禦漏洞,明天會解說Session,後天則會針對兩者進行比較。


上一篇
Day-13 HTTP狀態碼介紹
系列文
Izumi從零開始的30日WEB馬拉松14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言